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

804 + Improve 'no installers available' messaging #823

Merged
merged 21 commits into from
Feb 20, 2018
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;
}
5 changes: 3 additions & 2 deletions src/client/interpreter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,14 @@ export class InterpreterManager implements Disposable, IInterpreterService {
const pythonExecutableName = path.basename(fullyQualifiedPath);
const versionInfo = await this.serviceContainer.get<IInterpreterVersionService>(IInterpreterVersionService).getVersion(fullyQualifiedPath, pythonExecutableName);
const virtualEnvManager = this.serviceContainer.get<IVirtualEnvironmentManager>(IVirtualEnvironmentManager);
const virtualEnvName = await virtualEnvManager.detect(fullyQualifiedPath).then(env => env ? env.name : '');
const virtualEnv = await virtualEnvManager.detect(fullyQualifiedPath);
const virtualEnvName = virtualEnv ? virtualEnv.name : '';
const dislayNameSuffix = virtualEnvName.length > 0 ? ` (${virtualEnvName})` : '';
const displayName = `${versionInfo}${dislayNameSuffix}`;
return {
displayName,
path: fullyQualifiedPath,
type: InterpreterType.Unknown,
type: virtualEnv ? virtualEnv.type : InterpreterType.Unknown,
version: versionInfo
};
}
Expand Down
4 changes: 4 additions & 0 deletions src/client/interpreter/locators/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ export function fixInterpreterDisplayName(item: PythonInterpreter) {
}
return item;
}

export function isMacDefaultPythonPath(p: string) {
return p === 'python' || p === '/usr/bin/python';
}
Loading