Skip to content

Commit

Permalink
804 + Improve 'no installers available' messaging (#823)
Browse files Browse the repository at this point in the history
* Fix pylint search

* Handle quote escapes in strings

* Escapes in strings

* CR feedback

* Missing pip

* Test

* Tests

* Tests

* Mac python path

* Tests

* Tests

* Test

* "Go To Python object" doesn't work

* Proper detection and type population in virtual env

* Test fixes

* Discover pylintrc better + tests

* Undo change

* CR feedback

* Set interprereter before checking install
  • Loading branch information
Mikhail Arkhipov authored Feb 20, 2018
1 parent 3fe37a2 commit d44386e
Show file tree
Hide file tree
Showing 23 changed files with 442 additions and 126 deletions.
77 changes: 77 additions & 0 deletions src/client/common/installer/channelManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { inject, injectable } from 'inversify';
import { QuickPickItem, Uri } from 'vscode';
import { IInterpreterService, InterpreterType } from '../../interpreter/contracts';
import { IServiceContainer } from '../../ioc/types';
import { IApplicationShell } from '../application/types';
import { IPlatformService } from '../platform/types';
import { Product } from '../types';
import { ProductNames } from './productNames';
import { IInstallationChannelManager, IModuleInstaller } from './types';

@injectable()
export class InstallationChannelManager implements IInstallationChannelManager {
constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { }

public async getInstallationChannel(product: Product, resource?: Uri): Promise<IModuleInstaller | undefined> {
const channels = await this.getInstallationChannels(resource);
if (channels.length === 1) {
return channels[0];
}

const productName = ProductNames.get(product)!;
const appShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell);
if (channels.length === 0) {
await this.showNoInstallersMessage(resource);
return;
}

const placeHolder = `Select an option to install ${productName}`;
const options = channels.map(installer => {
return {
label: `Install using ${installer.displayName}`,
description: '',
installer
} as QuickPickItem & { installer: IModuleInstaller };
});
const selection = await appShell.showQuickPick(options, { matchOnDescription: true, matchOnDetail: true, placeHolder });
return selection ? selection.installer : undefined;
}

public async getInstallationChannels(resource?: Uri): Promise<IModuleInstaller[]> {
const installers = this.serviceContainer.getAll<IModuleInstaller>(IModuleInstaller);
const supportedInstallers: IModuleInstaller[] = [];
for (const mi of installers) {
if (await mi.isSupported(resource)) {
supportedInstallers.push(mi);
}
}
return supportedInstallers;
}

public async showNoInstallersMessage(resource?: Uri): Promise<void> {
const interpreters = this.serviceContainer.get<IInterpreterService>(IInterpreterService);
const interpreter = await interpreters.getActiveInterpreter(resource);
if (!interpreter) {
return; // Handled in the Python installation check.
}

const appShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell);
const search = 'Search for help';
let result: string | undefined;
if (interpreter.type === InterpreterType.Conda) {
result = await appShell.showErrorMessage('There is no Conda or Pip installer available in the selected environment.', search);
} else {
result = await appShell.showErrorMessage('There is no Pip installer available in the selected environment.', search);
}
if (result === search) {
const platform = this.serviceContainer.get<IPlatformService>(IPlatformService);
const osName = platform.isWindows
? 'Windows'
: (platform.isMac ? 'MacOS' : 'Linux');
appShell.openUrl(`https://www.bing.com/search?q=Install Pip ${osName} ${(interpreter.type === InterpreterType.Conda) ? 'Conda' : ''}`);
}
}
}
16 changes: 7 additions & 9 deletions src/client/common/installer/condaInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { inject, injectable } from 'inversify';
import { Uri } from 'vscode';
import { ICondaService, IInterpreterService, InterpreterType } from '../../interpreter/contracts';
import { ICondaService } from '../../interpreter/contracts';
import { IServiceContainer } from '../../ioc/types';
import { ExecutionInfo, IConfigurationService } from '../types';
import { ModuleInstaller } from './moduleInstaller';
Expand All @@ -15,7 +15,7 @@ export class CondaInstaller extends ModuleInstaller implements IModuleInstaller
public get displayName() {
return 'Conda';
}
constructor( @inject(IServiceContainer) serviceContainer: IServiceContainer) {
constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
super(serviceContainer);
}
/**
Expand All @@ -27,16 +27,14 @@ export class CondaInstaller extends ModuleInstaller implements IModuleInstaller
* @returns {Promise<boolean>} Whether conda is supported as a module installer or not.
*/
public async isSupported(resource?: Uri): Promise<boolean> {
if (typeof this.isCondaAvailable === 'boolean') {
if (this.isCondaAvailable !== undefined) {
return this.isCondaAvailable!;
}
const condaLocator = this.serviceContainer.get<ICondaService>(ICondaService);
const available = await condaLocator.isCondaAvailable();

if (!available) {
this.isCondaAvailable = await condaLocator.isCondaAvailable();
if (!this.isCondaAvailable) {
return false;
}

// Now we need to check if the current environment is a conda environment or not.
return this.isCurrentEnvironmentACondaEnvironment(resource);
}
Expand All @@ -48,11 +46,11 @@ export class CondaInstaller extends ModuleInstaller implements IModuleInstaller
const info = await condaService.getCondaEnvironment(pythonPath);
const args = ['install'];

if (info.name) {
if (info && info.name) {
// If we have the name of the conda environment, then use that.
args.push('--name');
args.push(info.name!);
} else if (info.path) {
} else if (info && info.path) {
// Else provide the full path to the environment path.
args.push('--prefix');
args.push(info.path);
Expand Down
35 changes: 6 additions & 29 deletions src/client/common/installer/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { IPlatformService } from '../platform/types';
import { IProcessService, IPythonExecutionFactory } from '../process/types';
import { ITerminalServiceFactory } from '../terminal/types';
import { IInstaller, ILogger, InstallerResponse, IOutputChannel, ModuleNamePurpose, Product } from '../types';
import { IModuleInstaller } from './types';
import { IInstallationChannelManager, IModuleInstaller } from './types';

export { Product } from '../types';

Expand Down Expand Up @@ -71,7 +71,7 @@ ProductTypes.set(Product.rope, ProductType.RefactoringLibrary);

@injectable()
export class Installer implements IInstaller {
constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer,
constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer,
@inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private outputChannel: vscode.OutputChannel) {
}
// tslint:disable-next-line:no-empty
Expand Down Expand Up @@ -135,7 +135,9 @@ export class Installer implements IInstaller {
if (product === Product.ctags) {
return this.installCTags();
}
const installer = await this.getInstallationChannel(product, resource);

const channels = this.serviceContainer.get<IInstallationChannelManager>(IInstallationChannelManager);
const installer = await channels.getInstallationChannel(product, resource);
if (!installer) {
return InstallerResponse.Ignore;
}
Expand Down Expand Up @@ -191,32 +193,7 @@ export class Installer implements IInstaller {
}
return InstallerResponse.Ignore;
}
private async getInstallationChannel(product: Product, resource?: Uri): Promise<IModuleInstaller | undefined> {
const productName = ProductNames.get(product)!;
const channels = await this.getInstallationChannels(resource);
if (channels.length === 0) {
window.showInformationMessage(`No installers available to install ${productName}.`);
return;
}
if (channels.length === 1) {
return channels[0];
}
const placeHolder = `Select an option to install ${productName}`;
const options = channels.map(installer => {
return {
label: `Install using ${installer.displayName}`,
description: '',
installer
} as QuickPickItem & { installer: IModuleInstaller };
});
const selection = await window.showQuickPick(options, { matchOnDescription: true, matchOnDetail: true, placeHolder });
return selection ? selection.installer : undefined;
}
private async getInstallationChannels(resource?: Uri): Promise<IModuleInstaller[]> {
const installers = this.serviceContainer.getAll<IModuleInstaller>(IModuleInstaller);
const supportedInstallers = await Promise.all(installers.map(async installer => installer.isSupported(resource).then(supported => supported ? installer : undefined)));
return supportedInstallers.filter(installer => installer !== undefined).map(installer => installer!);
}

// tslint:disable-next-line:no-any
private updateSetting(setting: string, value: any, resource?: Uri) {
if (resource && workspace.getWorkspaceFolder(resource)) {
Expand Down
51 changes: 5 additions & 46 deletions src/client/common/installer/productInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,13 @@ import { IPlatformService } from '../platform/types';
import { IProcessService, IPythonExecutionFactory } from '../process/types';
import { ITerminalServiceFactory } from '../terminal/types';
import { IConfigurationService, IInstaller, ILogger, InstallerResponse, IOutputChannel, ModuleNamePurpose, Product } from '../types';
import { IModuleInstaller } from './types';
import { ProductNames } from './productNames';
import { IInstallationChannelManager, IModuleInstaller } from './types';

export { Product } from '../types';

const CTagsInsllationScript = os.platform() === 'darwin' ? 'brew install ctags' : 'sudo apt-get install exuberant-ctags';

// tslint:disable-next-line:variable-name
const ProductNames = new Map<Product, string>();
ProductNames.set(Product.autopep8, 'autopep8');
ProductNames.set(Product.flake8, 'flake8');
ProductNames.set(Product.mypy, 'mypy');
ProductNames.set(Product.nosetest, 'nosetest');
ProductNames.set(Product.pep8, 'pep8');
ProductNames.set(Product.pylama, 'pylama');
ProductNames.set(Product.prospector, 'prospector');
ProductNames.set(Product.pydocstyle, 'pydocstyle');
ProductNames.set(Product.pylint, 'pylint');
ProductNames.set(Product.pytest, 'pytest');
ProductNames.set(Product.yapf, 'yapf');
ProductNames.set(Product.rope, 'rope');

enum ProductType {
Linter,
Formatter,
Expand All @@ -59,7 +45,8 @@ abstract class BaseInstaller {
return InstallerResponse.Installed;
}

const installer = await this.getInstallationChannel(product, resource);
const channels = this.serviceContainer.get<IInstallationChannelManager>(IInstallationChannelManager);
const installer = await channels.getInstallationChannel(product, resource);
if (!installer) {
return InstallerResponse.Ignore;
}
Expand Down Expand Up @@ -100,34 +87,6 @@ abstract class BaseInstaller {
protected getExecutableNameFromSettings(product: Product, resource?: Uri): string {
throw new Error('getExecutableNameFromSettings is not supported on this object');
}

private async getInstallationChannel(product: Product, resource?: Uri): Promise<IModuleInstaller | undefined> {
const productName = ProductNames.get(product)!;
const channels = await this.getInstallationChannels(resource);
if (channels.length === 0) {
window.showInformationMessage(`No installers available to install ${productName}.`);
return;
}
if (channels.length === 1) {
return channels[0];
}
const placeHolder = `Select an option to install ${productName}`;
const options = channels.map(installer => {
return {
label: `Install using ${installer.displayName}`,
description: '',
installer
} as QuickPickItem & { installer: IModuleInstaller };
});
const selection = await window.showQuickPick(options, { matchOnDescription: true, matchOnDetail: true, placeHolder });
return selection ? selection.installer : undefined;
}

private async getInstallationChannels(resource?: Uri): Promise<IModuleInstaller[]> {
const installers = this.serviceContainer.getAll<IModuleInstaller>(IModuleInstaller);
const supportedInstallers = await Promise.all(installers.map(async installer => installer.isSupported(resource).then(supported => supported ? installer : undefined)));
return supportedInstallers.filter(installer => installer !== undefined).map(installer => installer!);
}
}

class CTagsInstaller extends BaseInstaller {
Expand Down Expand Up @@ -253,7 +212,7 @@ class RefactoringLibraryInstaller extends BaseInstaller {
export class ProductInstaller implements IInstaller {
private ProductTypes = new Map<Product, ProductType>();

constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer,
constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer,
@inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private outputChannel: vscode.OutputChannel) {
this.ProductTypes.set(Product.flake8, ProductType.Linter);
this.ProductTypes.set(Product.mypy, ProductType.Linter);
Expand Down
19 changes: 19 additions & 0 deletions src/client/common/installer/productNames.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { Product } from '../types';

// tslint:disable-next-line:variable-name
export const ProductNames = new Map<Product, string>();
ProductNames.set(Product.autopep8, 'autopep8');
ProductNames.set(Product.flake8, 'flake8');
ProductNames.set(Product.mypy, 'mypy');
ProductNames.set(Product.nosetest, 'nosetest');
ProductNames.set(Product.pep8, 'pep8');
ProductNames.set(Product.pylama, 'pylama');
ProductNames.set(Product.prospector, 'prospector');
ProductNames.set(Product.pydocstyle, 'pydocstyle');
ProductNames.set(Product.pylint, 'pylint');
ProductNames.set(Product.pytest, 'pytest');
ProductNames.set(Product.yapf, 'yapf');
ProductNames.set(Product.rope, 'rope');
15 changes: 9 additions & 6 deletions src/client/common/installer/pythonInstallation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// Licensed under the MIT License.
'use strict';

import { IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE, InterpreterType } from '../../interpreter/contracts';
import { IInterpreterLocatorService, IInterpreterService, INTERPRETER_LOCATOR_SERVICE, InterpreterType } from '../../interpreter/contracts';
import { isMacDefaultPythonPath } from '../../interpreter/locators/helpers';
import { IServiceContainer } from '../../ioc/types';
import { IApplicationShell } from '../application/types';
import { IPlatformService } from '../platform/types';
Expand All @@ -15,7 +16,7 @@ export class PythonInstaller {
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) {
Expand All @@ -24,10 +25,12 @@ export class PythonInstaller {
const interpreters = await this.locator.getInterpreters();
if (interpreters.length > 0) {
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');
if (platform.isMac && isMacDefaultPythonPath(settings.pythonPath)) {
const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService);
const interpreter = await interpreterService.getActiveInterpreter();
if (interpreter && interpreter.type === InterpreterType.Unknown) {
await this.shell.showWarningMessage('Selected interpreter is macOS system Python which is not recommended. Please select different interpreter');
}
}
return true;
}
Expand Down
4 changes: 3 additions & 1 deletion src/client/common/installer/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
'use strict';

import { IServiceManager } from '../../ioc/types';
import { InstallationChannelManager } from './channelManager';
import { CondaInstaller } from './condaInstaller';
import { PipInstaller } from './pipInstaller';
import { IModuleInstaller } from './types';
import { IInstallationChannelManager, IModuleInstaller } from './types';

export function registerTypes(serviceManager: IServiceManager) {
serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, CondaInstaller);
serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PipInstaller);
serviceManager.addSingleton<IInstallationChannelManager>(IInstallationChannelManager, InstallationChannelManager);
}
8 changes: 8 additions & 0 deletions src/client/common/installer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

import { Uri } from 'vscode';
import { Product } from '../types';

export const IModuleInstaller = Symbol('IModuleInstaller');
export interface IModuleInstaller {
Expand All @@ -14,3 +15,10 @@ export const IPythonInstallation = Symbol('IPythonInstallation');
export interface IPythonInstallation {
checkInstallation(): Promise<boolean>;
}

export const IInstallationChannelManager = Symbol('IInstallationChannelManager');
export interface IInstallationChannelManager {
getInstallationChannel(product: Product, resource?: Uri): Promise<IModuleInstaller | undefined>;
getInstallationChannels(resource?: Uri): Promise<IModuleInstaller[]>;
showNoInstallersMessage(): void;
}
8 changes: 4 additions & 4 deletions src/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,14 @@ export async function activate(context: vscode.ExtensionContext) {
sortImports.activate(context, standardOutputChannel, serviceContainer);
const interpreterManager = serviceContainer.get<IInterpreterService>(IInterpreterService);

// This must be completed before we can continue.
interpreterManager.initialize();
await interpreterManager.autoSetInterpreter();

const pythonInstaller = new PythonInstaller(serviceContainer);
pythonInstaller.checkPythonInstallation(PythonSettings.getInstance())
.catch(ex => console.error('Python Extension: pythonInstaller.checkPythonInstallation', ex));

// This must be completed before we can continue.
await interpreterManager.autoSetInterpreter();

interpreterManager.initialize();
interpreterManager.refresh()
.catch(ex => console.error('Python Extension: interpreterManager.refresh', ex));

Expand Down
Loading

0 comments on commit d44386e

Please sign in to comment.