diff --git a/src/client/common/installer/channelManager.ts b/src/client/common/installer/channelManager.ts new file mode 100644 index 000000000000..3ee00df21419 --- /dev/null +++ b/src/client/common/installer/channelManager.ts @@ -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 { + const channels = await this.getInstallationChannels(resource); + if (channels.length === 1) { + return channels[0]; + } + + const productName = ProductNames.get(product)!; + const appShell = this.serviceContainer.get(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 { + const installers = this.serviceContainer.getAll(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 { + const interpreters = this.serviceContainer.get(IInterpreterService); + const interpreter = await interpreters.getActiveInterpreter(resource); + if (!interpreter) { + return; // Handled in the Python installation check. + } + + const appShell = this.serviceContainer.get(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); + 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' : ''}`); + } + } +} diff --git a/src/client/common/installer/condaInstaller.ts b/src/client/common/installer/condaInstaller.ts index 58ca0bddcdbf..0c279d169510 100644 --- a/src/client/common/installer/condaInstaller.ts +++ b/src/client/common/installer/condaInstaller.ts @@ -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'; @@ -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); } /** @@ -27,16 +27,14 @@ export class CondaInstaller extends ModuleInstaller implements IModuleInstaller * @returns {Promise} Whether conda is supported as a module installer or not. */ public async isSupported(resource?: Uri): Promise { - if (typeof this.isCondaAvailable === 'boolean') { + if (this.isCondaAvailable !== undefined) { return this.isCondaAvailable!; } const condaLocator = this.serviceContainer.get(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); } @@ -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); diff --git a/src/client/common/installer/installer.ts b/src/client/common/installer/installer.ts index 0409af1fa737..473305f59bc3 100644 --- a/src/client/common/installer/installer.ts +++ b/src/client/common/installer/installer.ts @@ -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'; @@ -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 @@ -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); + const installer = await channels.getInstallationChannel(product, resource); if (!installer) { return InstallerResponse.Ignore; } @@ -191,32 +193,7 @@ export class Installer implements IInstaller { } return InstallerResponse.Ignore; } - private async getInstallationChannel(product: Product, resource?: Uri): Promise { - 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 { - const installers = this.serviceContainer.getAll(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)) { diff --git a/src/client/common/installer/productInstaller.ts b/src/client/common/installer/productInstaller.ts index 3991a882a4ff..d54c7a95ae31 100644 --- a/src/client/common/installer/productInstaller.ts +++ b/src/client/common/installer/productInstaller.ts @@ -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(); -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, @@ -59,7 +45,8 @@ abstract class BaseInstaller { return InstallerResponse.Installed; } - const installer = await this.getInstallationChannel(product, resource); + const channels = this.serviceContainer.get(IInstallationChannelManager); + const installer = await channels.getInstallationChannel(product, resource); if (!installer) { return InstallerResponse.Ignore; } @@ -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 { - 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 { - const installers = this.serviceContainer.getAll(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 { @@ -253,7 +212,7 @@ class RefactoringLibraryInstaller extends BaseInstaller { export class ProductInstaller implements IInstaller { private ProductTypes = new Map(); - 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); diff --git a/src/client/common/installer/productNames.ts b/src/client/common/installer/productNames.ts new file mode 100644 index 000000000000..9371f540c778 --- /dev/null +++ b/src/client/common/installer/productNames.ts @@ -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(); +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'); diff --git a/src/client/common/installer/pythonInstallation.ts b/src/client/common/installer/pythonInstallation.ts index bf3bd1384370..1463404eb0ab 100644 --- a/src/client/common/installer/pythonInstallation.ts +++ b/src/client/common/installer/pythonInstallation.ts @@ -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'; @@ -15,7 +16,7 @@ export class PythonInstaller { constructor(private serviceContainer: IServiceContainer) { this.locator = serviceContainer.get(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); this.shell = serviceContainer.get(IApplicationShell); - } + } public async checkPythonInstallation(settings: IPythonSettings): Promise { if (settings.disableInstallationChecks === true) { @@ -24,10 +25,12 @@ export class PythonInstaller { const interpreters = await this.locator.getInterpreters(); if (interpreters.length > 0) { const platform = this.serviceContainer.get(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); + 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; } diff --git a/src/client/common/installer/serviceRegistry.ts b/src/client/common/installer/serviceRegistry.ts index c53d457a3829..0282d033fd0a 100644 --- a/src/client/common/installer/serviceRegistry.ts +++ b/src/client/common/installer/serviceRegistry.ts @@ -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, CondaInstaller); serviceManager.addSingleton(IModuleInstaller, PipInstaller); + serviceManager.addSingleton(IInstallationChannelManager, InstallationChannelManager); } diff --git a/src/client/common/installer/types.ts b/src/client/common/installer/types.ts index a1759606de8b..7058dbe1c463 100644 --- a/src/client/common/installer/types.ts +++ b/src/client/common/installer/types.ts @@ -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 { @@ -14,3 +15,10 @@ export const IPythonInstallation = Symbol('IPythonInstallation'); export interface IPythonInstallation { checkInstallation(): Promise; } + +export const IInstallationChannelManager = Symbol('IInstallationChannelManager'); +export interface IInstallationChannelManager { + getInstallationChannel(product: Product, resource?: Uri): Promise; + getInstallationChannels(resource?: Uri): Promise; + showNoInstallersMessage(): void; +} diff --git a/src/client/extension.ts b/src/client/extension.ts index 21657960b7ca..4c3cb509d6de 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -103,14 +103,14 @@ export async function activate(context: vscode.ExtensionContext) { sortImports.activate(context, standardOutputChannel, serviceContainer); const interpreterManager = serviceContainer.get(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)); diff --git a/src/client/interpreter/index.ts b/src/client/interpreter/index.ts index 3eddee641c28..3c23ef5d807e 100644 --- a/src/client/interpreter/index.ts +++ b/src/client/interpreter/index.ts @@ -102,13 +102,14 @@ export class InterpreterManager implements Disposable, IInterpreterService { const pythonExecutableName = path.basename(fullyQualifiedPath); const versionInfo = await this.serviceContainer.get(IInterpreterVersionService).getVersion(fullyQualifiedPath, pythonExecutableName); const virtualEnvManager = this.serviceContainer.get(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 }; } diff --git a/src/client/interpreter/locators/helpers.ts b/src/client/interpreter/locators/helpers.ts index 4efeb34d9721..1b5cf698fcbe 100644 --- a/src/client/interpreter/locators/helpers.ts +++ b/src/client/interpreter/locators/helpers.ts @@ -22,3 +22,7 @@ export function fixInterpreterDisplayName(item: PythonInterpreter) { } return item; } + +export function isMacDefaultPythonPath(p: string) { + return p === 'python' || p === '/usr/bin/python'; +} diff --git a/src/client/interpreter/locators/index.ts b/src/client/interpreter/locators/index.ts index 3645e15cee14..c48040036777 100644 --- a/src/client/interpreter/locators/index.ts +++ b/src/client/interpreter/locators/index.ts @@ -18,7 +18,7 @@ import { WINDOWS_REGISTRY_SERVICE, WORKSPACE_VIRTUAL_ENV_SERVICE } from '../contracts'; -import { fixInterpreterDisplayName } from './helpers'; +import { fixInterpreterDisplayName, isMacDefaultPythonPath } from './helpers'; @injectable() export class PythonInterpreterLocatorService implements IInterpreterLocatorService { @@ -45,7 +45,7 @@ export class PythonInterpreterLocatorService implements IInterpreterLocatorServi .map(fixInterpreterDisplayName) .map(item => { item.path = path.normalize(item.path); return item; }) .reduce((accumulator, current) => { - if (this.platform.isMac && current.path === '/usr/bin/python') { + if (this.platform.isMac && isMacDefaultPythonPath(current.path)) { return accumulator; } const existingItem = accumulator.find(item => arePathsSame(item.path, current.path)); diff --git a/src/client/interpreter/locators/services/baseVirtualEnvService.ts b/src/client/interpreter/locators/services/baseVirtualEnvService.ts index a060bf8502da..d6000631acde 100644 --- a/src/client/interpreter/locators/services/baseVirtualEnvService.ts +++ b/src/client/interpreter/locators/services/baseVirtualEnvService.ts @@ -54,10 +54,7 @@ export class BaseVirtualEnvService extends CacheableLocatorService { .then(dirs => { const scriptOrBinDirs = dirs.filter(dir => { const folderName = path.basename(dir); - // Perform case insistive search on windows. - // On windows its named eitgher 'Scripts' or 'scripts'. - const folderNameToCheck = isWindows ? folderName.toUpperCase() : folderName; - return folderNameToCheck === dirToLookFor; + return this.fileSystem.arePathsSame(folderName, dirToLookFor); }); return scriptOrBinDirs.length === 1 ? scriptOrBinDirs[0] : ''; }) diff --git a/src/client/interpreter/locators/services/currentPathService.ts b/src/client/interpreter/locators/services/currentPathService.ts index 62d934ff5634..62374b78859c 100644 --- a/src/client/interpreter/locators/services/currentPathService.ts +++ b/src/client/interpreter/locators/services/currentPathService.ts @@ -11,7 +11,7 @@ import { CacheableLocatorService } from './cacheableLocatorService'; @injectable() export class CurrentPathService extends CacheableLocatorService { - public constructor( @inject(IVirtualEnvironmentManager) private virtualEnvMgr: IVirtualEnvironmentManager, + public constructor(@inject(IVirtualEnvironmentManager) private virtualEnvMgr: IVirtualEnvironmentManager, @inject(IInterpreterVersionService) private versionProvider: IInterpreterVersionService, @inject(IProcessService) private processService: IProcessService, @inject(IServiceContainer) serviceContainer: IServiceContainer) { @@ -34,17 +34,17 @@ export class CurrentPathService extends CacheableLocatorService { // tslint:disable-next-line:promise-function-async .then(interpreters => Promise.all(interpreters.map(interpreter => this.getInterpreterDetails(interpreter)))); } - private async getInterpreterDetails(interpreter: string) { + private async getInterpreterDetails(interpreter: string): Promise { return Promise.all([ this.versionProvider.getVersion(interpreter, path.basename(interpreter)), this.virtualEnvMgr.detect(interpreter) - ]) - .then(([displayName, virtualEnv]) => { + ]). + then(([displayName, virtualEnv]) => { displayName += virtualEnv ? ` (${virtualEnv.name})` : ''; return { displayName, path: interpreter, - type: InterpreterType.Unknown + type: virtualEnv ? virtualEnv.type : InterpreterType.Unknown }; }); } diff --git a/src/client/interpreter/virtualEnvs/index.ts b/src/client/interpreter/virtualEnvs/index.ts index d0a9bd1634f6..01a538fbb012 100644 --- a/src/client/interpreter/virtualEnvs/index.ts +++ b/src/client/interpreter/virtualEnvs/index.ts @@ -3,9 +3,9 @@ import { IVirtualEnvironmentIdentifier, IVirtualEnvironmentManager } from './typ @injectable() export class VirtualEnvironmentManager implements IVirtualEnvironmentManager { - constructor( @multiInject(IVirtualEnvironmentIdentifier) private envs: IVirtualEnvironmentIdentifier[]) { + constructor(@multiInject(IVirtualEnvironmentIdentifier) private envs: IVirtualEnvironmentIdentifier[]) { } - public detect(pythonPath: string): Promise { + public detect(pythonPath: string): Promise { const promises = this.envs .map(item => item.detect(pythonPath) .then(result => { diff --git a/src/client/interpreter/virtualEnvs/types.ts b/src/client/interpreter/virtualEnvs/types.ts index 710507358999..10c44f9e8e96 100644 --- a/src/client/interpreter/virtualEnvs/types.ts +++ b/src/client/interpreter/virtualEnvs/types.ts @@ -11,5 +11,5 @@ export interface IVirtualEnvironmentIdentifier { } export const IVirtualEnvironmentManager = Symbol('VirtualEnvironmentManager'); export interface IVirtualEnvironmentManager { - detect(pythonPath: string): Promise; + detect(pythonPath: string): Promise; } diff --git a/src/client/interpreter/virtualEnvs/venv.ts b/src/client/interpreter/virtualEnvs/venv.ts index 934014c2d6c4..3fca7bb4f2c0 100644 --- a/src/client/interpreter/virtualEnvs/venv.ts +++ b/src/client/interpreter/virtualEnvs/venv.ts @@ -1,6 +1,10 @@ -import { injectable } from 'inversify'; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; import * as path from 'path'; -import { fsExistsAsync } from '../../common/utils'; +import { IFileSystem } from '../../common/platform/types'; +import { IServiceContainer } from '../../ioc/types'; import { InterpreterType } from '../contracts'; import { IVirtualEnvironmentIdentifier } from './types'; @@ -10,9 +14,14 @@ const pyEnvCfgFileName = 'pyvenv.cfg'; export class VEnv implements IVirtualEnvironmentIdentifier { public readonly name: string = 'venv'; public readonly type = InterpreterType.VEnv; + private fs: IFileSystem; + + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.fs = serviceContainer.get(IFileSystem); + } public detect(pythonPath: string): Promise { const dir = path.dirname(pythonPath); const pyEnvCfgPath = path.join(dir, '..', pyEnvCfgFileName); - return fsExistsAsync(pyEnvCfgPath); + return this.fs.fileExistsAsync(pyEnvCfgPath); } } diff --git a/src/client/interpreter/virtualEnvs/virtualEnv.ts b/src/client/interpreter/virtualEnvs/virtualEnv.ts index e2c78f9e2e6e..e1dada31a344 100644 --- a/src/client/interpreter/virtualEnvs/virtualEnv.ts +++ b/src/client/interpreter/virtualEnvs/virtualEnv.ts @@ -1,18 +1,27 @@ -import { injectable } from 'inversify'; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; import * as path from 'path'; -import { fsExistsAsync } from '../../common/utils'; +import { IFileSystem } from '../../common/platform/types'; +import { IServiceContainer } from '../../ioc/types'; import { InterpreterType } from '../contracts'; import { IVirtualEnvironmentIdentifier } from './types'; -const OrigPrefixFile = 'orig-prefix.txt'; - @injectable() export class VirtualEnv implements IVirtualEnvironmentIdentifier { public readonly name: string = 'virtualenv'; public readonly type = InterpreterType.VirtualEnv; - public detect(pythonPath: string): Promise { + private fs: IFileSystem; + + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.fs = serviceContainer.get(IFileSystem); + } + public async detect(pythonPath: string): Promise { const dir = path.dirname(pythonPath); - const origPrefixFile = path.join(dir, '..', 'lib', OrigPrefixFile); - return fsExistsAsync(origPrefixFile); + const libExists = await this.fs.directoryExistsAsync(path.join(dir, '..', 'lib')); + const binExists = await this.fs.directoryExistsAsync(path.join(dir, '..', 'bin')); + const includeExists = await this.fs.directoryExistsAsync(path.join(dir, '..', 'include')); + return libExists && binExists && includeExists; } } diff --git a/src/client/languageServices/jediProxyFactory.ts b/src/client/languageServices/jediProxyFactory.ts index 8ac6b04349d5..5e18b2396e51 100644 --- a/src/client/languageServices/jediProxyFactory.ts +++ b/src/client/languageServices/jediProxyFactory.ts @@ -15,8 +15,8 @@ export class JediFactory implements Disposable { this.disposables.forEach(disposable => disposable.dispose()); this.disposables = []; } - public getJediProxyHandler(resource: Uri): JediProxyHandler { - const workspaceFolder = workspace.getWorkspaceFolder(resource); + public getJediProxyHandler(resource?: Uri): JediProxyHandler { + const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined; let workspacePath = workspaceFolder ? workspaceFolder.uri.fsPath : undefined; if (!workspacePath) { if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index 4af691063219..d00258561d34 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -5,16 +5,17 @@ import { IApplicationShell } from '../../client/common/application/types'; import { ConfigurationService } from '../../client/common/configuration/service'; import { EnumEx } from '../../client/common/enumUtils'; import { createDeferred } from '../../client/common/helpers'; +import { InstallationChannelManager } from '../../client/common/installer/channelManager'; import { ProductInstaller } from '../../client/common/installer/productInstaller'; -import { IModuleInstaller } from '../../client/common/installer/types'; +import { IInstallationChannelManager, IModuleInstaller } from '../../client/common/installer/types'; import { Logger } from '../../client/common/logger'; import { PersistentStateFactory } from '../../client/common/persistentState'; import { PathUtils } from '../../client/common/platform/pathUtils'; import { CurrentProcess } from '../../client/common/process/currentProcess'; import { IProcessService } from '../../client/common/process/types'; import { IConfigurationService, ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, IsWindows, ModuleNamePurpose, Product } from '../../client/common/types'; -import { updateSetting } from '../common'; import { rootWorkspaceUri } from '../common'; +import { updateSetting } from '../common'; import { MockModuleInstaller } from '../mocks/moduleInstaller'; import { MockProcessService } from '../mocks/proc'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; @@ -53,6 +54,7 @@ suite('Installer', () => { ioc.serviceManager.addSingleton(IInstaller, ProductInstaller); ioc.serviceManager.addSingleton(IPathUtils, PathUtils); ioc.serviceManager.addSingleton(ICurrentProcess, CurrentProcess); + ioc.serviceManager.addSingleton(IInstallationChannelManager, InstallationChannelManager); ioc.serviceManager.addSingletonInstance(IApplicationShell, TypeMoq.Mock.ofType().object); ioc.serviceManager.addSingleton(IConfigurationService, ConfigurationService); diff --git a/src/test/install/channelManager.channels.test.ts b/src/test/install/channelManager.channels.test.ts new file mode 100644 index 000000000000..4c238aa20b43 --- /dev/null +++ b/src/test/install/channelManager.channels.test.ts @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { Container } from 'inversify'; +import * as TypeMoq from 'typemoq'; +import { QuickPickOptions } from 'vscode'; +import { IApplicationShell } from '../../client/common/application/types'; +import { InstallationChannelManager } from '../../client/common/installer/channelManager'; +import { IModuleInstaller } from '../../client/common/installer/types'; +import { Product } from '../../client/common/types'; +import { ServiceContainer } from '../../client/ioc/container'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import { IServiceContainer } from '../../client/ioc/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Installation - installation channels', () => { + let serviceManager: ServiceManager; + let serviceContainer: IServiceContainer; + + setup(() => { + const cont = new Container(); + serviceManager = new ServiceManager(cont); + serviceContainer = new ServiceContainer(cont); + }); + + test('Single channel', async () => { + const installer = mockInstaller(true, ''); + const cm = new InstallationChannelManager(serviceContainer); + const channels = await cm.getInstallationChannels(); + assert.equal(channels.length, 1, 'Incorrect number of channels'); + assert.equal(channels[0], installer.object, 'Incorrect installer'); + }); + + test('Multiple channels', async () => { + const installer1 = mockInstaller(true, '1'); + mockInstaller(false, '2'); + const installer3 = mockInstaller(true, '3'); + + const cm = new InstallationChannelManager(serviceContainer); + const channels = await cm.getInstallationChannels(); + assert.equal(channels.length, 2, 'Incorrect number of channels'); + assert.equal(channels[0], installer1.object, 'Incorrect installer 1'); + assert.equal(channels[1], installer3.object, 'Incorrect installer 2'); + }); + + test('Select installer', async () => { + const installer1 = mockInstaller(true, '1'); + const installer2 = mockInstaller(true, '2'); + + const appShell = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(IApplicationShell, appShell.object); + + // tslint:disable-next-line:no-any + let items: any[] | undefined; + appShell + .setup(x => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((i: string[], o: QuickPickOptions) => { + items = i; + }) + .returns(() => new Promise((resolve, reject) => resolve(undefined))); + + installer1.setup(x => x.displayName).returns(() => 'Name 1'); + installer2.setup(x => x.displayName).returns(() => 'Name 2'); + + const cm = new InstallationChannelManager(serviceContainer); + await cm.getInstallationChannel(Product.pylint); + + assert.notEqual(items, undefined, 'showQuickPick not called'); + assert.equal(items!.length, 2, 'Incorrect number of installer shown'); + assert.notEqual(items![0]!.label!.indexOf('Name 1'), -1, 'Incorrect first installer name'); + assert.notEqual(items![1]!.label!.indexOf('Name 2'), -1, 'Incorrect second installer name'); + }); + + function mockInstaller(supported: boolean, name: string): TypeMoq.IMock { + const installer = TypeMoq.Mock.ofType(); + installer + .setup(x => x.isSupported(TypeMoq.It.isAny())) + .returns(() => new Promise((resolve) => resolve(supported))); + serviceManager.addSingletonInstance(IModuleInstaller, installer.object, name); + return installer; + } +}); diff --git a/src/test/install/channelManager.messages.test.ts b/src/test/install/channelManager.messages.test.ts new file mode 100644 index 000000000000..0762b4242014 --- /dev/null +++ b/src/test/install/channelManager.messages.test.ts @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { Container } from 'inversify'; +import * as TypeMoq from 'typemoq'; +import { IApplicationShell } from '../../client/common/application/types'; +import { InstallationChannelManager } from '../../client/common/installer/channelManager'; +import { IPlatformService } from '../../client/common/platform/types'; +import { Product } from '../../client/common/types'; +import { IInterpreterService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; +import { ServiceContainer } from '../../client/ioc/container'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import { IServiceContainer } from '../../client/ioc/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Installation - channel messages', () => { + let serviceContainer: IServiceContainer; + let platform: TypeMoq.IMock; + let appShell: TypeMoq.IMock; + let interpreters: TypeMoq.IMock; + + setup(() => { + const cont = new Container(); + const serviceManager = new ServiceManager(cont); + serviceContainer = new ServiceContainer(cont); + + platform = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(IPlatformService, platform.object); + + appShell = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(IApplicationShell, appShell.object); + + interpreters = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(IInterpreterService, interpreters.object); + }); + + test('No installers message: Unknown/Windows', async () => { + platform.setup(x => x.isWindows).returns(() => true); + await testInstallerMissingMessage(InterpreterType.Unknown, + async (channels: InstallationChannelManager, message: string, url: string) => { + await channels.showNoInstallersMessage(); + verifyMessage(message, ['Pip'], ['Conda']); + verifyUrl(url, ['Windows', 'Pip']); + }); + }); + + test('No installers message: Conda/Windows', async () => { + platform.setup(x => x.isWindows).returns(() => true); + await testInstallerMissingMessage(InterpreterType.Conda, + async (channels: InstallationChannelManager, message: string, url: string) => { + await channels.showNoInstallersMessage(); + verifyMessage(message, ['Pip', 'Conda'], []); + verifyUrl(url, ['Windows', 'Pip', 'Conda']); + }); + }); + + test('No installers message: Unknown/Mac', async () => { + platform.setup(x => x.isWindows).returns(() => false); + platform.setup(x => x.isMac).returns(() => true); + await testInstallerMissingMessage(InterpreterType.Unknown, + async (channels: InstallationChannelManager, message: string, url: string) => { + await channels.showNoInstallersMessage(); + verifyMessage(message, ['Pip'], ['Conda']); + verifyUrl(url, ['Mac', 'Pip']); + }); + }); + + test('No installers message: Conda/Mac', async () => { + platform.setup(x => x.isWindows).returns(() => false); + platform.setup(x => x.isMac).returns(() => true); + await testInstallerMissingMessage(InterpreterType.Conda, + async (channels: InstallationChannelManager, message: string, url: string) => { + await channels.showNoInstallersMessage(); + verifyMessage(message, ['Pip', 'Conda'], []); + verifyUrl(url, ['Mac', 'Pip', 'Conda']); + }); + }); + + test('No installers message: Unknown/Linux', async () => { + platform.setup(x => x.isWindows).returns(() => false); + platform.setup(x => x.isMac).returns(() => false); + platform.setup(x => x.isLinux).returns(() => true); + await testInstallerMissingMessage(InterpreterType.Unknown, + async (channels: InstallationChannelManager, message: string, url: string) => { + await channels.showNoInstallersMessage(); + verifyMessage(message, ['Pip'], ['Conda']); + verifyUrl(url, ['Linux', 'Pip']); + }); + }); + + test('No installers message: Conda/Linux', async () => { + platform.setup(x => x.isWindows).returns(() => false); + platform.setup(x => x.isMac).returns(() => false); + platform.setup(x => x.isLinux).returns(() => true); + await testInstallerMissingMessage(InterpreterType.Conda, + async (channels: InstallationChannelManager, message: string, url: string) => { + await channels.showNoInstallersMessage(); + verifyMessage(message, ['Pip', 'Conda'], []); + verifyUrl(url, ['Linux', 'Pip', 'Conda']); + }); + }); + + test('No channels message', async () => { + platform.setup(x => x.isWindows).returns(() => true); + await testInstallerMissingMessage(InterpreterType.Unknown, + async (channels: InstallationChannelManager, message: string, url: string) => { + await channels.getInstallationChannel(Product.pylint); + verifyMessage(message, ['Pip'], ['Conda']); + verifyUrl(url, ['Windows', 'Pip']); + }); + }); + + function verifyMessage(message: string, present: string[], missing: string[]) { + for (const p of present) { + assert.equal(message.indexOf(p) >= 0, true, `Message does not contain ${p}.`); + } + for (const m of missing) { + assert.equal(message.indexOf(m) < 0, true, `Message incorrectly contains ${m}.`); + } + } + + function verifyUrl(url: string, terms: string[]) { + assert.equal(url.indexOf('https://') >= 0, true, 'Search Url must be https.'); + for (const term of terms) { + assert.equal(url.indexOf(term) >= 0, true, `Search Url does not contain ${term}.`); + } + } + + async function testInstallerMissingMessage( + interpreterType: InterpreterType, + verify: (c: InstallationChannelManager, m: string, u: string) => void): Promise { + + const activeInterpreter: PythonInterpreter = { + type: interpreterType, + path: '' + }; + interpreters + .setup(x => x.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => new Promise((resolve, reject) => resolve(activeInterpreter))); + const channels = new InstallationChannelManager(serviceContainer); + + let url: string = ''; + let message: string = ''; + let search: string = ''; + appShell + .setup(x => x.showErrorMessage(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) + .callback((m: string, s: string) => { + message = m; + search = s; + }) + .returns(() => new Promise((resolve, reject) => resolve(search))); + appShell.setup(x => x.openUrl(TypeMoq.It.isAnyString())).callback((s: string) => { + url = s; + }); + verify(channels, message, url); + } +}); diff --git a/src/test/install/pythonInstallation.test.ts b/src/test/install/pythonInstallation.test.ts index f3114b961f8c..4df7dfdd4fb2 100644 --- a/src/test/install/pythonInstallation.test.ts +++ b/src/test/install/pythonInstallation.test.ts @@ -9,7 +9,7 @@ import { IApplicationShell } from '../../client/common/application/types'; import { PythonInstaller } from '../../client/common/installer/pythonInstallation'; import { IPlatformService } from '../../client/common/platform/types'; import { IPythonSettings } from '../../client/common/types'; -import { IInterpreterLocatorService } from '../../client/interpreter/contracts'; +import { IInterpreterLocatorService, IInterpreterService } from '../../client/interpreter/contracts'; import { InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; @@ -35,9 +35,19 @@ class TestContext { this.locator = TypeMoq.Mock.ofType(); this.settings = TypeMoq.Mock.ofType(); + const activeInterpreter: PythonInterpreter = { + type: InterpreterType.Unknown, + path: '' + }; + const interpreterService = TypeMoq.Mock.ofType(); + interpreterService + .setup(x => x.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => new Promise((resolve, reject) => resolve(activeInterpreter))); + this.serviceManager.addSingletonInstance(IPlatformService, this.platform.object); this.serviceManager.addSingletonInstance(IApplicationShell, this.appShell.object); this.serviceManager.addSingletonInstance(IInterpreterLocatorService, this.locator.object); + this.serviceManager.addSingletonInstance(IInterpreterService, interpreterService.object); this.pythonInstaller = new PythonInstaller(this.serviceContainer); this.platform.setup(x => x.isMac).returns(() => isMac);