diff --git a/package.json b/package.json index 5eed4680c9a9..8840c2573fa8 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.17.0" + "vscode": "^1.18.0" }, "recommendations": [ "donjayamanne.jupyter" @@ -1748,6 +1748,7 @@ "inversify": "^4.5.2", "line-by-line": "^0.1.5", "lodash": "^4.17.4", + "md5": "^2.2.1", "minimatch": "^3.0.3", "named-js-regexp": "^1.3.1", "opn": "^5.1.0", @@ -1781,11 +1782,13 @@ "@types/iconv-lite": "0.0.1", "@types/istanbul": "^0.4.29", "@types/lodash": "^4.14.74", + "@types/md5": "^2.1.32", "@types/mocha": "^2.2.43", "@types/node": "^6.0.40", "@types/semver": "^5.4.0", "@types/shortid": "0.0.29", "@types/sinon": "^2.3.2", + "@types/untildify": "^3.0.0", "@types/uuid": "^3.3.27", "@types/winreg": "^1.2.30", "@types/xml2js": "^0.4.0", diff --git a/src/client/common/application/applicationShell.ts b/src/client/common/application/applicationShell.ts index d905d1085741..ac3330651b55 100644 --- a/src/client/common/application/applicationShell.ts +++ b/src/client/common/application/applicationShell.ts @@ -2,21 +2,21 @@ // Licensed under the MIT License. 'use strict'; -// tslint:disable-next-line:no-require-imports no-var-requires +// tslint:disable:no-require-imports no-var-requires no-any unified-signatures const opn = require('opn'); import { injectable } from 'inversify'; import * as vscode from 'vscode'; +import { Disposable, StatusBarAlignment, StatusBarItem, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode'; import { IApplicationShell } from './types'; @injectable() export class ApplicationShell implements IApplicationShell { - public showInformationMessage(message: string, ...items: string[]): Thenable ; - public showInformationMessage(message: string, options: vscode.MessageOptions, ...items: string[]): Thenable ; - public showInformationMessage(message: string, ...items: T[]): Thenable ; - public showInformationMessage(message: string, options: vscode.MessageOptions, ...items: T[]): Thenable ; - // tslint:disable-next-line:no-any - public showInformationMessage(message: string, options?: any, ...items: any[]): Thenable { + public showInformationMessage(message: string, ...items: string[]): Thenable; + public showInformationMessage(message: string, options: vscode.MessageOptions, ...items: string[]): Thenable; + public showInformationMessage(message: string, ...items: T[]): Thenable; + public showInformationMessage(message: string, options: vscode.MessageOptions, ...items: T[]): Thenable; + public showInformationMessage(message: string, options?: any, ...items: any[]): Thenable { return vscode.window.showInformationMessage(message, options, ...items); } @@ -24,7 +24,6 @@ export class ApplicationShell implements IApplicationShell { public showWarningMessage(message: string, options: vscode.MessageOptions, ...items: string[]): Thenable; public showWarningMessage(message: string, ...items: T[]): Thenable; public showWarningMessage(message: string, options: vscode.MessageOptions, ...items: T[]): Thenable; - // tslint:disable-next-line:no-any public showWarningMessage(message: any, options?: any, ...items: any[]) { return vscode.window.showWarningMessage(message, options, ...items); } @@ -33,28 +32,41 @@ export class ApplicationShell implements IApplicationShell { public showErrorMessage(message: string, options: vscode.MessageOptions, ...items: string[]): Thenable; public showErrorMessage(message: string, ...items: T[]): Thenable; public showErrorMessage(message: string, options: vscode.MessageOptions, ...items: T[]): Thenable; - // tslint:disable-next-line:no-any public showErrorMessage(message: any, options?: any, ...items: any[]) { return vscode.window.showErrorMessage(message, options, ...items); } public showQuickPick(items: string[] | Thenable, options?: vscode.QuickPickOptions, token?: vscode.CancellationToken): Thenable; public showQuickPick(items: T[] | Thenable, options?: vscode.QuickPickOptions, token?: vscode.CancellationToken): Thenable; - // tslint:disable-next-line:no-any public showQuickPick(items: any, options?: any, token?: any) { return vscode.window.showQuickPick(items, options, token); } - public showOpenDialog(options: vscode.OpenDialogOptions): Thenable { + public showOpenDialog(options: vscode.OpenDialogOptions): Thenable { return vscode.window.showOpenDialog(options); } - public showSaveDialog(options: vscode.SaveDialogOptions): Thenable { + public showSaveDialog(options: vscode.SaveDialogOptions): Thenable { return vscode.window.showSaveDialog(options); } - public showInputBox(options?: vscode.InputBoxOptions, token?: vscode.CancellationToken): Thenable { + public showInputBox(options?: vscode.InputBoxOptions, token?: vscode.CancellationToken): Thenable { return vscode.window.showInputBox(options, token); } public openUrl(url: string): void { opn(url); } + + public setStatusBarMessage(text: string, hideAfterTimeout: number): Disposable; + public setStatusBarMessage(text: string, hideWhenDone: Thenable): Disposable; + public setStatusBarMessage(text: string): Disposable; + public setStatusBarMessage(text: string, arg?: any): Disposable { + return vscode.window.setStatusBarMessage(text, arg); + } + + public createStatusBarItem(alignment?: StatusBarAlignment, priority?: number): StatusBarItem { + return vscode.window.createStatusBarItem(alignment, priority); + } + public showWorkspaceFolderPick(options?: WorkspaceFolderPickOptions): Thenable { + return vscode.window.showWorkspaceFolderPick(options); + } + } diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 4cd05f99592c..9f012d1c3384 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -5,8 +5,9 @@ // tslint:disable:no-any unified-signatures import * as vscode from 'vscode'; -import { CancellationToken, Disposable, Event, FileSystemWatcher, GlobPattern, TextDocument, TextDocumentShowOptions, WorkspaceConfiguration } from 'vscode'; +import { CancellationToken, ConfigurationChangeEvent, Disposable, Event, FileSystemWatcher, GlobPattern, TextDocument, TextDocumentShowOptions, WorkspaceConfiguration, WorkspaceFolderPickOptions } from 'vscode'; import { TextEditor, TextEditorEdit, TextEditorOptionsChangeEvent, TextEditorSelectionChangeEvent, TextEditorViewColumnChangeEvent } from 'vscode'; +import { StatusBarAlignment, StatusBarItem } from 'vscode'; import { Uri, ViewColumn, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; import { Terminal, TerminalOptions } from 'vscode'; @@ -197,6 +198,55 @@ export interface IApplicationShell { * @param url Url to open. */ openUrl(url: string): void; + + /** + * Set a message to the status bar. This is a short hand for the more powerful + * status bar [items](#window.createStatusBarItem). + * + * @param text The message to show, supports icon substitution as in status bar [items](#StatusBarItem.text). + * @param hideAfterTimeout Timeout in milliseconds after which the message will be disposed. + * @return A disposable which hides the status bar message. + */ + setStatusBarMessage(text: string, hideAfterTimeout: number): Disposable; + + /** + * Set a message to the status bar. This is a short hand for the more powerful + * status bar [items](#window.createStatusBarItem). + * + * @param text The message to show, supports icon substitution as in status bar [items](#StatusBarItem.text). + * @param hideWhenDone Thenable on which completion (resolve or reject) the message will be disposed. + * @return A disposable which hides the status bar message. + */ + setStatusBarMessage(text: string, hideWhenDone: Thenable): Disposable; + + /** + * Set a message to the status bar. This is a short hand for the more powerful + * status bar [items](#window.createStatusBarItem). + * + * *Note* that status bar messages stack and that they must be disposed when no + * longer used. + * + * @param text The message to show, supports icon substitution as in status bar [items](#StatusBarItem.text). + * @return A disposable which hides the status bar message. + */ + setStatusBarMessage(text: string): Disposable; + + /** + * Creates a status bar [item](#StatusBarItem). + * + * @param alignment The alignment of the item. + * @param priority The priority of the item. Higher values mean the item should be shown more to the left. + * @return A new status bar item. + */ + createStatusBarItem(alignment?: StatusBarAlignment, priority?: number): StatusBarItem; + /** + * Shows a selection list of [workspace folders](#workspace.workspaceFolders) to pick from. + * Returns `undefined` if no folder is open. + * + * @param options Configures the behavior of the workspace folder list. + * @return A promise that resolves to the workspace folder or `undefined`. + */ + showWorkspaceFolderPick(options?: WorkspaceFolderPickOptions): Thenable; } export const ICommandManager = Symbol('ICommandManager'); @@ -371,7 +421,7 @@ export interface IWorkspaceService { /** * An event that is emitted when the [configuration](#WorkspaceConfiguration) changed. */ - readonly onDidChangeConfiguration: Event; + readonly onDidChangeConfiguration: Event; /** * Returns the [workspace folder](#WorkspaceFolder) that contains a given uri. diff --git a/src/client/common/application/workspace.ts b/src/client/common/application/workspace.ts index 71d183ba854b..54f2c7dd2fa1 100644 --- a/src/client/common/application/workspace.ts +++ b/src/client/common/application/workspace.ts @@ -3,11 +3,12 @@ import { injectable } from 'inversify'; import * as vscode from 'vscode'; +import { ConfigurationChangeEvent } from 'vscode'; import { IWorkspaceService } from './types'; @injectable() export class WorkspaceService implements IWorkspaceService { - public get onDidChangeConfiguration(): vscode.Event { + public get onDidChangeConfiguration(): vscode.Event { return vscode.workspace.onDidChangeConfiguration; } public get rootPath(): string | undefined { diff --git a/src/client/common/process/pythonExecutionFactory.ts b/src/client/common/process/pythonExecutionFactory.ts index 32ab2566c132..4866e6e504f8 100644 --- a/src/client/common/process/pythonExecutionFactory.ts +++ b/src/client/common/process/pythonExecutionFactory.ts @@ -11,13 +11,13 @@ import { IPythonExecutionFactory, IPythonExecutionService } from './types'; @injectable() export class PythonExecutionFactory implements IPythonExecutionFactory { private envVarsService: IEnvironmentVariablesProvider; - constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.envVarsService = serviceContainer.get(IEnvironmentVariablesProvider); - } + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.envVarsService = serviceContainer.get(IEnvironmentVariablesProvider); + } public async create(resource?: Uri): Promise { return this.envVarsService.getEnvironmentVariables(resource) .then(customEnvVars => { - return new PythonExecutionService(this.serviceContainer, customEnvVars); + return new PythonExecutionService(this.serviceContainer, customEnvVars, resource); }); } } diff --git a/src/client/common/process/pythonProcess.ts b/src/client/common/process/pythonProcess.ts index 92d0f8fecdc7..a8a502f9a1c4 100644 --- a/src/client/common/process/pythonProcess.ts +++ b/src/client/common/process/pythonProcess.ts @@ -2,9 +2,11 @@ // Licensed under the MIT License. import { injectable } from 'inversify'; +import { Uri } from 'vscode'; import { IServiceContainer } from '../../ioc/types'; import { ErrorUtils } from '../errors/errorUtils'; import { ModuleNotInstalledError } from '../errors/moduleNotInstalledError'; +import { IFileSystem } from '../platform/types'; import { IConfigurationService } from '../types'; import { EnvironmentVariables } from '../variables/types'; import { ExecutionResult, IProcessService, IPythonExecutionService, ObservableExecutionResult, SpawnOptions } from './types'; @@ -13,10 +15,12 @@ import { ExecutionResult, IProcessService, IPythonExecutionService, ObservableEx export class PythonExecutionService implements IPythonExecutionService { private procService: IProcessService; private configService: IConfigurationService; + private fileSystem: IFileSystem; - constructor(serviceContainer: IServiceContainer, private envVars: EnvironmentVariables | undefined) { + constructor(serviceContainer: IServiceContainer, private envVars: EnvironmentVariables | undefined, private resource?: Uri) { this.procService = serviceContainer.get(IProcessService); this.configService = serviceContainer.get(IConfigurationService); + this.fileSystem = serviceContainer.get(IFileSystem); } public async getVersion(): Promise { @@ -24,6 +28,11 @@ export class PythonExecutionService implements IPythonExecutionService { .then(output => output.stdout.trim()); } public async getExecutablePath(): Promise { + // If we've passed the python file, then return the file. + // This is because on mac if using the interpreter /usr/bin/python2.7 we can get a different value for the path + if (await this.fileSystem.fileExistsAsync(this.pythonPath)){ + return this.pythonPath; + } return this.procService.exec(this.pythonPath, ['-c', 'import sys;print(sys.executable)'], { env: this.envVars, throwOnStdErr: true }) .then(output => output.stdout.trim()); } @@ -71,6 +80,6 @@ export class PythonExecutionService implements IPythonExecutionService { return result; } private get pythonPath(): string { - return this.configService.getSettings().pythonPath; + return this.configService.getSettings(this.resource).pythonPath; } } diff --git a/src/client/debugger/Common/Utils.ts b/src/client/debugger/Common/Utils.ts index 106901c2de05..6d6d785f7e36 100644 --- a/src/client/debugger/Common/Utils.ts +++ b/src/client/debugger/Common/Utils.ts @@ -4,7 +4,7 @@ import * as child_process from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; -import * as untildify from 'untildify'; +import untildify = require('untildify'); import { IPythonModule, IPythonProcess, IPythonThread } from './Contracts'; export const IS_WINDOWS = /^win/.test(process.platform); diff --git a/src/client/extension.ts b/src/client/extension.ts index e9cc4ee998bd..a8d15d4bbb7d 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -18,7 +18,6 @@ import { PythonInstaller } from './common/installer/pythonInstallation'; import { registerTypes as installerRegisterTypes } from './common/installer/serviceRegistry'; import { registerTypes as platformRegisterTypes } from './common/platform/serviceRegistry'; import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry'; -import { IProcessService } from './common/process/types'; import { registerTypes as commonRegisterTypes } from './common/serviceRegistry'; import { GLOBAL_MEMENTO, IDisposableRegistry, ILogger, IMemento, IOutputChannel, IPersistentStateFactory, WORKSPACE_MEMENTO } from './common/types'; import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry'; @@ -26,9 +25,8 @@ import { BaseConfigurationProvider } from './debugger/configProviders/baseProvid import { registerTypes as debugConfigurationRegisterTypes } from './debugger/configProviders/serviceRegistry'; import { IDebugConfigurationProvider } from './debugger/types'; import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry'; -import { InterpreterSelector } from './interpreter/configuration/interpreterSelector'; -import { ICondaService, IInterpreterService, IInterpreterVersionService } from './interpreter/contracts'; -import { ShebangCodeLensProvider } from './interpreter/display/shebangCodeLensProvider'; +import { IInterpreterSelector } from './interpreter/configuration/types'; +import { ICondaService, IInterpreterService, IShebangCodeLensProvider } from './interpreter/contracts'; import { registerTypes as interpretersRegisterTypes } from './interpreter/serviceRegistry'; import { ServiceContainer } from './ioc/container'; import { ServiceManager } from './ioc/serviceManager'; @@ -116,9 +114,7 @@ export async function activate(context: vscode.ExtensionContext) { interpreterManager.refresh() .catch(ex => console.error('Python Extension: interpreterManager.refresh', ex)); - const processService = serviceContainer.get(IProcessService); - const interpreterVersionService = serviceContainer.get(IInterpreterVersionService); - context.subscriptions.push(new InterpreterSelector(interpreterManager, interpreterVersionService, processService)); + context.subscriptions.push(serviceContainer.get(IInterpreterSelector)); context.subscriptions.push(activateUpdateSparkLibraryProvider()); activateSimplePythonRefactorProvider(context, standardOutputChannel, serviceContainer); const jediFactory = new JediFactory(context.asAbsolutePath('.'), serviceContainer); @@ -156,7 +152,7 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.languages.registerHoverProvider(PYTHON, new PythonHoverProvider(jediFactory))); context.subscriptions.push(vscode.languages.registerReferenceProvider(PYTHON, new PythonReferenceProvider(jediFactory))); context.subscriptions.push(vscode.languages.registerCompletionItemProvider(PYTHON, new PythonCompletionItemProvider(jediFactory, serviceContainer), '.')); - context.subscriptions.push(vscode.languages.registerCodeLensProvider(PYTHON, new ShebangCodeLensProvider(processService))); + context.subscriptions.push(vscode.languages.registerCodeLensProvider(PYTHON, serviceContainer.get(IShebangCodeLensProvider))); const symbolProvider = new PythonSymbolProvider(jediFactory); context.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(PYTHON, symbolProvider)); diff --git a/src/client/interpreter/configuration/interpreterSelector.ts b/src/client/interpreter/configuration/interpreterSelector.ts index 0d3acb8a89e7..6dc9dc0c7033 100644 --- a/src/client/interpreter/configuration/interpreterSelector.ts +++ b/src/client/interpreter/configuration/interpreterSelector.ts @@ -1,41 +1,50 @@ +import { inject, injectable } from 'inversify'; import * as path from 'path'; -import { commands, ConfigurationTarget, Disposable, QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode'; +import { ConfigurationTarget, Disposable, QuickPickItem, QuickPickOptions, Uri } from 'vscode'; +import { IApplicationShell, ICommandManager, IDocumentManager, IWorkspaceService } from '../../common/application/types'; import * as settings from '../../common/configSettings'; import { Commands } from '../../common/constants'; -import { IProcessService } from '../../common/process/types'; -import { IInterpreterService, IInterpreterVersionService, PythonInterpreter, WorkspacePythonPath } from '../contracts'; -import { ShebangCodeLensProvider } from '../display/shebangCodeLensProvider'; -import { PythonPathUpdaterService } from './pythonPathUpdaterService'; -import { PythonPathUpdaterServiceFactory } from './pythonPathUpdaterServiceFactory'; +import { IServiceContainer } from '../../ioc/types'; +import { IInterpreterService, IShebangCodeLensProvider, PythonInterpreter, WorkspacePythonPath } from '../contracts'; +import { IInterpreterSelector, IPythonPathUpdaterServiceManager } from './types'; interface IInterpreterQuickPickItem extends QuickPickItem { path: string; } -export class InterpreterSelector implements Disposable { +@injectable() +export class InterpreterSelector implements IInterpreterSelector { private disposables: Disposable[] = []; - private pythonPathUpdaterService: PythonPathUpdaterService; - constructor(private interpreterManager: IInterpreterService, - interpreterVersionService: IInterpreterVersionService, - private processService: IProcessService) { - this.disposables.push(commands.registerCommand(Commands.Set_Interpreter, this.setInterpreter.bind(this))); - this.disposables.push(commands.registerCommand(Commands.Set_ShebangInterpreter, this.setShebangInterpreter.bind(this))); - this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), interpreterVersionService); + private pythonPathUpdaterService: IPythonPathUpdaterServiceManager; + private readonly interpreterManager: IInterpreterService; + private readonly workspaceService: IWorkspaceService; + private readonly applicationShell: IApplicationShell; + private readonly documentManager: IDocumentManager; + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.interpreterManager = serviceContainer.get(IInterpreterService); + this.workspaceService = this.serviceContainer.get(IWorkspaceService); + this.applicationShell = this.serviceContainer.get(IApplicationShell); + this.documentManager = this.serviceContainer.get(IDocumentManager); + + const commandManager = serviceContainer.get(ICommandManager); + this.disposables.push(commandManager.registerCommand(Commands.Set_Interpreter, this.setInterpreter.bind(this))); + this.disposables.push(commandManager.registerCommand(Commands.Set_ShebangInterpreter, this.setShebangInterpreter.bind(this))); + this.pythonPathUpdaterService = serviceContainer.get(IPythonPathUpdaterServiceManager); } public dispose() { this.disposables.forEach(disposable => disposable.dispose()); } private async getWorkspaceToSetPythonPath(): Promise { - if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + if (!Array.isArray(this.workspaceService.workspaceFolders) || this.workspaceService.workspaceFolders.length === 0) { return undefined; } - if (workspace.workspaceFolders.length === 1) { - return { folderUri: workspace.workspaceFolders[0].uri, configTarget: ConfigurationTarget.Workspace }; + if (this.workspaceService.workspaceFolders.length === 1) { + return { folderUri: this.workspaceService.workspaceFolders[0].uri, configTarget: ConfigurationTarget.Workspace }; } // Ok we have multiple interpreters, get the user to pick a folder. - // tslint:disable-next-line:no-any prefer-type-cast - const workspaceFolder = await (window as any).showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); + const applicationShell = this.serviceContainer.get(IApplicationShell); + const workspaceFolder = await applicationShell.showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); return workspaceFolder ? { folderUri: workspaceFolder.uri, configTarget: ConfigurationTarget.WorkspaceFolder } : undefined; } private async suggestionToQuickPickItem(suggestion: PythonInterpreter, workspaceUri?: Uri): Promise { @@ -61,7 +70,7 @@ export class InterpreterSelector implements Disposable { } private async setInterpreter() { - const setInterpreterGlobally = !Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0; + const setInterpreterGlobally = !Array.isArray(this.workspaceService.workspaceFolders) || this.workspaceService.workspaceFolders.length === 0; let configTarget = ConfigurationTarget.Global; let wkspace: Uri | undefined; if (!setInterpreterGlobally) { @@ -84,21 +93,22 @@ export class InterpreterSelector implements Disposable { placeHolder: `current: ${currentPythonPath}` }; - const selection = await window.showQuickPick(suggestions, quickPickOptions); + const selection = await this.applicationShell.showQuickPick(suggestions, quickPickOptions); if (selection !== undefined) { await this.pythonPathUpdaterService.updatePythonPath(selection.path, configTarget, 'ui', wkspace); } } private async setShebangInterpreter(): Promise { - const shebang = await new ShebangCodeLensProvider(this.processService).detectShebang(window.activeTextEditor!.document); + const shebangCodeLensProvider = this.serviceContainer.get(IShebangCodeLensProvider); + const shebang = await shebangCodeLensProvider.detectShebang(this.documentManager.activeTextEditor!.document); if (!shebang) { return; } - const isGlobalChange = !Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0; - const workspaceFolder = workspace.getWorkspaceFolder(window.activeTextEditor!.document.uri); - const isWorkspaceChange = Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length === 1; + const isGlobalChange = !Array.isArray(this.workspaceService.workspaceFolders) || this.workspaceService.workspaceFolders.length === 0; + const workspaceFolder = this.workspaceService.getWorkspaceFolder(this.documentManager.activeTextEditor!.document.uri); + const isWorkspaceChange = Array.isArray(this.workspaceService.workspaceFolders) && this.workspaceService.workspaceFolders.length === 1; if (isGlobalChange) { await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Global, 'shebang'); @@ -106,7 +116,7 @@ export class InterpreterSelector implements Disposable { } if (isWorkspaceChange || !workspaceFolder) { - await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Workspace, 'shebang', workspace.workspaceFolders![0].uri); + await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Workspace, 'shebang', this.workspaceService.workspaceFolders![0].uri); return; } diff --git a/src/client/interpreter/configuration/pythonPathUpdaterService.ts b/src/client/interpreter/configuration/pythonPathUpdaterService.ts index ff74f3112bdf..67a6e0f5009d 100644 --- a/src/client/interpreter/configuration/pythonPathUpdaterService.ts +++ b/src/client/interpreter/configuration/pythonPathUpdaterService.ts @@ -1,15 +1,22 @@ +import { inject, injectable } from 'inversify'; import * as path from 'path'; import { ConfigurationTarget, Uri, window } from 'vscode'; +import { IServiceContainer } from '../../ioc/types'; import { sendTelemetryEvent } from '../../telemetry'; import { PYTHON_INTERPRETER } from '../../telemetry/constants'; import { StopWatch } from '../../telemetry/stopWatch'; import { PythonInterpreterTelemetry } from '../../telemetry/types'; import { IInterpreterVersionService } from '../contracts'; -import { IPythonPathUpdaterServiceFactory } from './types'; +import { IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager } from './types'; -export class PythonPathUpdaterService { - constructor(private pythonPathSettingsUpdaterFactory: IPythonPathUpdaterServiceFactory, - private interpreterVersionService: IInterpreterVersionService) { } +@injectable() +export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManager { + private readonly pythonPathSettingsUpdaterFactory: IPythonPathUpdaterServiceFactory; + private readonly interpreterVersionService: IInterpreterVersionService; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.pythonPathSettingsUpdaterFactory = serviceContainer.get(IPythonPathUpdaterServiceFactory); + this.interpreterVersionService = serviceContainer.get(IInterpreterVersionService); + } public async updatePythonPath(pythonPath: string, configTarget: ConfigurationTarget, trigger: 'ui' | 'shebang' | 'load', wkspace?: Uri): Promise { const stopWatch = new StopWatch(); const pythonPathUpdater = this.getPythonUpdaterService(configTarget, wkspace); diff --git a/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts b/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts index c48d20cdac1c..d7e6451aeb40 100644 --- a/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts +++ b/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts @@ -1,17 +1,25 @@ +import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../common/application/types'; +import { IServiceContainer } from '../../ioc/types'; import { GlobalPythonPathUpdaterService } from './services/globalUpdaterService'; import { WorkspaceFolderPythonPathUpdaterService } from './services/workspaceFolderUpdaterService'; import { WorkspacePythonPathUpdaterService } from './services/workspaceUpdaterService'; import { IPythonPathUpdaterService, IPythonPathUpdaterServiceFactory } from './types'; +@injectable() export class PythonPathUpdaterServiceFactory implements IPythonPathUpdaterServiceFactory { + private readonly workspaceService: IWorkspaceService; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.workspaceService = serviceContainer.get(IWorkspaceService); + } public getGlobalPythonPathConfigurationService(): IPythonPathUpdaterService { - return new GlobalPythonPathUpdaterService(); + return new GlobalPythonPathUpdaterService(this.workspaceService); } public getWorkspacePythonPathConfigurationService(wkspace: Uri): IPythonPathUpdaterService { - return new WorkspacePythonPathUpdaterService(wkspace); + return new WorkspacePythonPathUpdaterService(wkspace, this.workspaceService); } public getWorkspaceFolderPythonPathConfigurationService(workspaceFolder: Uri): IPythonPathUpdaterService { - return new WorkspaceFolderPythonPathUpdaterService(workspaceFolder); + return new WorkspaceFolderPythonPathUpdaterService(workspaceFolder, this.workspaceService); } } diff --git a/src/client/interpreter/configuration/services/globalUpdaterService.ts b/src/client/interpreter/configuration/services/globalUpdaterService.ts index d25a866bfbcd..8fffa5c77c8c 100644 --- a/src/client/interpreter/configuration/services/globalUpdaterService.ts +++ b/src/client/interpreter/configuration/services/globalUpdaterService.ts @@ -1,9 +1,10 @@ -import { workspace } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; import { IPythonPathUpdaterService } from '../types'; export class GlobalPythonPathUpdaterService implements IPythonPathUpdaterService { + constructor(private readonly workspaceService: IWorkspaceService) { } public async updatePythonPath(pythonPath: string): Promise { - const pythonConfig = workspace.getConfiguration('python'); + const pythonConfig = this.workspaceService.getConfiguration('python'); const pythonPathValue = pythonConfig.inspect('pythonPath'); if (pythonPathValue && pythonPathValue.globalValue === pythonPath) { diff --git a/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts index 38224d7c4e50..26f819952fb9 100644 --- a/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts +++ b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts @@ -1,12 +1,13 @@ import * as path from 'path'; -import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; import { IPythonPathUpdaterService } from '../types'; export class WorkspaceFolderPythonPathUpdaterService implements IPythonPathUpdaterService { - constructor(private workspaceFolder: Uri) { + constructor(private workspaceFolder: Uri, private readonly workspaceService: IWorkspaceService) { } public async updatePythonPath(pythonPath: string): Promise { - const pythonConfig = workspace.getConfiguration('python', this.workspaceFolder); + const pythonConfig = this.workspaceService.getConfiguration('python', this.workspaceFolder); const pythonPathValue = pythonConfig.inspect('pythonPath'); if (pythonPathValue && pythonPathValue.workspaceFolderValue === pythonPath) { diff --git a/src/client/interpreter/configuration/services/workspaceUpdaterService.ts b/src/client/interpreter/configuration/services/workspaceUpdaterService.ts index 51c0644c503e..7a5b8bdca28f 100644 --- a/src/client/interpreter/configuration/services/workspaceUpdaterService.ts +++ b/src/client/interpreter/configuration/services/workspaceUpdaterService.ts @@ -1,20 +1,21 @@ import * as path from 'path'; -import { Uri, workspace } from 'vscode'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; import { IPythonPathUpdaterService } from '../types'; export class WorkspacePythonPathUpdaterService implements IPythonPathUpdaterService { - constructor(private wkspace: Uri) { + constructor(private workspace: Uri, private readonly workspaceService: IWorkspaceService) { } public async updatePythonPath(pythonPath: string): Promise { - const pythonConfig = workspace.getConfiguration('python', this.wkspace); + const pythonConfig = this.workspaceService.getConfiguration('python', this.workspace); const pythonPathValue = pythonConfig.inspect('pythonPath'); if (pythonPathValue && pythonPathValue.workspaceValue === pythonPath) { return; } - if (pythonPath.startsWith(this.wkspace.fsPath)) { + if (pythonPath.startsWith(this.workspace.fsPath)) { // tslint:disable-next-line:no-invalid-template-strings - pythonPath = path.join('${workspaceFolder}', path.relative(this.wkspace.fsPath, pythonPath)); + pythonPath = path.join('${workspaceFolder}', path.relative(this.workspace.fsPath, pythonPath)); } await pythonConfig.update('pythonPath', pythonPath, false); } diff --git a/src/client/interpreter/configuration/types.ts b/src/client/interpreter/configuration/types.ts index 05825416f3bc..43ec286f77f0 100644 --- a/src/client/interpreter/configuration/types.ts +++ b/src/client/interpreter/configuration/types.ts @@ -1,12 +1,22 @@ -import { Uri } from 'vscode'; -import { IPythonPathUpdaterService } from './types'; +import { ConfigurationTarget, Disposable, Uri } from 'vscode'; export interface IPythonPathUpdaterService { updatePythonPath(pythonPath: string): Promise; } +export const IPythonPathUpdaterServiceFactory = Symbol('IPythonPathUpdaterServiceFactory'); export interface IPythonPathUpdaterServiceFactory { getGlobalPythonPathConfigurationService(): IPythonPathUpdaterService; getWorkspacePythonPathConfigurationService(wkspace: Uri): IPythonPathUpdaterService; getWorkspaceFolderPythonPathConfigurationService(workspaceFolder: Uri): IPythonPathUpdaterService; } + +export const IPythonPathUpdaterServiceManager = Symbol('IPythonPathUpdaterServiceManager'); +export interface IPythonPathUpdaterServiceManager { + updatePythonPath(pythonPath: string, configTarget: ConfigurationTarget, trigger: 'ui' | 'shebang' | 'load', wkspace?: Uri): Promise; +} + +export const IInterpreterSelector = Symbol('IInterpreterSelector'); +export interface IInterpreterSelector extends Disposable { + +} diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index 70c52be3260b..919ea8c46ab6 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -1,4 +1,4 @@ -import { ConfigurationTarget, Disposable, Uri } from 'vscode'; +import { CodeLensProvider, ConfigurationTarget, Disposable, TextDocument, Uri } from 'vscode'; import { Architecture } from '../common/platform/types'; export const INTERPRETER_LOCATOR_SERVICE = 'IInterpreterLocatorService'; @@ -7,7 +7,8 @@ export const CONDA_ENV_FILE_SERVICE = 'CondaEnvFileService'; export const CONDA_ENV_SERVICE = 'CondaEnvService'; export const CURRENT_PATH_SERVICE = 'CurrentPathService'; export const KNOWN_PATH_SERVICE = 'KnownPathsService'; -export const VIRTUAL_ENV_SERVICE = 'VirtualEnvService'; +export const GLOBAL_VIRTUAL_ENV_SERVICE = 'VirtualEnvService'; +export const WORKSPACE_VIRTUAL_ENV_SERVICE = 'WorkspaceVirtualEnvService'; export const IInterpreterVersionService = Symbol('IInterpreterVersionService'); export interface IInterpreterVersionService { @@ -16,8 +17,11 @@ export interface IInterpreterVersionService { } export const IKnownSearchPathsForInterpreters = Symbol('IKnownSearchPathsForInterpreters'); -export const IKnownSearchPathsForVirtualEnvironments = Symbol('IKnownSearchPathsForVirtualEnvironments'); +export const IVirtualEnvironmentsSearchPathProvider = Symbol('IVirtualEnvironmentsSearchPathProvider'); +export interface IVirtualEnvironmentsSearchPathProvider { + getSearchPaths(resource?: Uri): string[]; +} export const IInterpreterLocatorService = Symbol('IInterpreterLocatorService'); export interface IInterpreterLocatorService extends Disposable { @@ -67,7 +71,6 @@ export type PythonInterpreter = { export type WorkspacePythonPath = { folderUri: Uri; - pytonPath?: string; configTarget: ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder; }; @@ -80,3 +83,18 @@ export interface IInterpreterService { refresh(): Promise; initialize(): void; } + +export const IInterpreterDisplay = Symbol('IInterpreterDisplay'); +export interface IInterpreterDisplay { + refresh(resource?: Uri): Promise; +} + +export const IShebangCodeLensProvider = Symbol('IShebangCodeLensProvider'); +export interface IShebangCodeLensProvider extends CodeLensProvider { + detectShebang(document: TextDocument): Promise; +} + +export const IInterpreterHelper = Symbol('IInterpreterHelper'); +export interface IInterpreterHelper { + getActiveWorkspaceUri(): WorkspacePythonPath | undefined; +} diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index f852747d5003..e728e60e78ef 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -1,32 +1,64 @@ +import { inject, injectable } from 'inversify'; import { EOL } from 'os'; import * as path from 'path'; -import { Disposable, StatusBarItem, Uri } from 'vscode'; -import { PythonSettings } from '../../common/configSettings'; -import * as utils from '../../common/utils'; -import { IInterpreterService, IInterpreterVersionService } from '../contracts'; -import { getActiveWorkspaceUri } from '../helpers'; +import { Disposable, StatusBarAlignment, StatusBarItem, Uri } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; +import { IFileSystem } from '../../common/platform/types'; +import { IConfigurationService, IDisposableRegistry } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService, IInterpreterVersionService } from '../contracts'; import { IVirtualEnvironmentManager } from '../virtualEnvs/types'; // tslint:disable-next-line:completed-docs -export class InterpreterDisplay implements Disposable { - constructor(private statusBar: StatusBarItem, - private interpreterService: IInterpreterService, - private virtualEnvMgr: IVirtualEnvironmentManager, - private versionProvider: IInterpreterVersionService) { +@injectable() +export class InterpreterDisplay implements IInterpreterDisplay { + private readonly statusBar: StatusBarItem; + private readonly interpreterService: IInterpreterService; + private readonly virtualEnvMgr: IVirtualEnvironmentManager; + private readonly versionProvider: IInterpreterVersionService; + private readonly fileSystem: IFileSystem; + private readonly configurationService: IConfigurationService; + private readonly helper: IInterpreterHelper; + private readonly workspaceService: IWorkspaceService; + private currentWorkspaceInterpreter?: Uri; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.interpreterService = serviceContainer.get(IInterpreterService); + this.virtualEnvMgr = serviceContainer.get(IVirtualEnvironmentManager); + this.versionProvider = serviceContainer.get(IInterpreterVersionService); + this.fileSystem = serviceContainer.get(IFileSystem); + this.configurationService = serviceContainer.get(IConfigurationService); + this.helper = serviceContainer.get(IInterpreterHelper); + this.workspaceService = serviceContainer.get(IWorkspaceService); + const application = serviceContainer.get(IApplicationShell); + const disposableRegistry = serviceContainer.get(IDisposableRegistry); + + this.statusBar = application.createStatusBarItem(StatusBarAlignment.Left); this.statusBar.command = 'python.setInterpreter'; + disposableRegistry.push(this.statusBar); } - public dispose() { - // + public async refresh(resource?: Uri) { + // Use the workspace Uri if available + if (resource && this.workspaceService.getWorkspaceFolder(resource)) { + resource = this.workspaceService.getWorkspaceFolder(resource)!.uri; + } + if (!resource) { + const wkspc = this.helper.getActiveWorkspaceUri(); + resource = wkspc ? wkspc.folderUri : undefined; + } + await this.updateDisplay(resource); } - public async refresh() { - const wkspc = getActiveWorkspaceUri(); - await this.updateDisplay(wkspc ? wkspc.folderUri : undefined); + private shouldRefresh(workspaceFolder?: Uri) { + if (!workspaceFolder || !this.currentWorkspaceInterpreter) { + return true; + } + return !this.fileSystem.arePathsSame(workspaceFolder.fsPath, this.currentWorkspaceInterpreter.fsPath); } - private async updateDisplay(resource?: Uri) { - const interpreters = await this.interpreterService.getInterpreters(resource); - const interpreter = await this.interpreterService.getActiveInterpreter(resource); - const pythonPath = interpreter ? interpreter.path : PythonSettings.getInstance(resource).pythonPath; + private async updateDisplay(workspaceFolder?: Uri) { + this.currentWorkspaceInterpreter = workspaceFolder; + const interpreters = await this.interpreterService.getInterpreters(workspaceFolder); + const interpreter = await this.interpreterService.getActiveInterpreter(workspaceFolder); + const pythonPath = interpreter ? interpreter.path : this.configurationService.getSettings(workspaceFolder).pythonPath; this.statusBar.color = ''; this.statusBar.tooltip = pythonPath; @@ -40,7 +72,7 @@ export class InterpreterDisplay implements Disposable { } else { const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; await Promise.all([ - utils.fsExistsAsync(pythonPath), + this.fileSystem.fileExistsAsync(pythonPath), this.versionProvider.getVersion(pythonPath, defaultDisplayName), this.getVirtualEnvironmentName(pythonPath) ]) diff --git a/src/client/interpreter/display/shebangCodeLensProvider.ts b/src/client/interpreter/display/shebangCodeLensProvider.ts index 134e3fa9e68b..8bb2595e752b 100644 --- a/src/client/interpreter/display/shebangCodeLensProvider.ts +++ b/src/client/interpreter/display/shebangCodeLensProvider.ts @@ -1,13 +1,20 @@ +import { inject, injectable } from 'inversify'; import * as vscode from 'vscode'; import { CancellationToken, CodeLens, TextDocument } from 'vscode'; import * as settings from '../../common/configSettings'; import { IProcessService } from '../../common/process/types'; import { IS_WINDOWS } from '../../common/utils'; +import { IServiceContainer } from '../../ioc/types'; +import { IShebangCodeLensProvider } from '../contracts'; -export class ShebangCodeLensProvider implements vscode.CodeLensProvider { +@injectable() +export class ShebangCodeLensProvider implements IShebangCodeLensProvider { // tslint:disable-next-line:no-any public onDidChangeCodeLenses: vscode.Event = vscode.workspace.onDidChangeConfiguration as any as vscode.Event; - constructor(private processService: IProcessService) { } + private readonly processService: IProcessService; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.processService = serviceContainer.get(IProcessService); + } public async detectShebang(document: TextDocument): Promise { const firstLine = document.lineAt(0); if (firstLine.isEmptyOrWhitespace) { diff --git a/src/client/interpreter/helpers.ts b/src/client/interpreter/helpers.ts index dc8308344d2f..ba272607a147 100644 --- a/src/client/interpreter/helpers.ts +++ b/src/client/interpreter/helpers.ts @@ -1,5 +1,8 @@ -import { ConfigurationTarget, window, workspace } from 'vscode'; -import { WorkspacePythonPath } from './contracts'; +import { inject, injectable } from 'inversify'; +import { ConfigurationTarget } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../common/application/types'; +import { IServiceContainer } from '../ioc/types'; +import { IInterpreterHelper, WorkspacePythonPath } from './contracts'; export function getFirstNonEmptyLineFromMultilineString(stdout: string) { if (!stdout) { @@ -8,18 +11,26 @@ export function getFirstNonEmptyLineFromMultilineString(stdout: string) { const lines = stdout.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0); return lines.length > 0 ? lines[0] : ''; } -export function getActiveWorkspaceUri(): WorkspacePythonPath | undefined { - if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { - return undefined; - } - if (workspace.workspaceFolders.length === 1) { - return { folderUri: workspace.workspaceFolders[0].uri, configTarget: ConfigurationTarget.Workspace }; + +@injectable() +export class InterpreterHelper implements IInterpreterHelper { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } - if (window.activeTextEditor) { - const workspaceFolder = workspace.getWorkspaceFolder(window.activeTextEditor.document.uri); - if (workspaceFolder) { - return { configTarget: ConfigurationTarget.WorkspaceFolder, folderUri: workspaceFolder.uri }; + public getActiveWorkspaceUri(): WorkspacePythonPath | undefined { + const workspaceService = this.serviceContainer.get(IWorkspaceService); + const documentManager = this.serviceContainer.get(IDocumentManager); + + if (!Array.isArray(workspaceService.workspaceFolders) || workspaceService.workspaceFolders.length === 0) { + return; + } + if (workspaceService.workspaceFolders.length === 1) { + return { folderUri: workspaceService.workspaceFolders[0].uri, configTarget: ConfigurationTarget.Workspace }; + } + if (documentManager.activeTextEditor) { + const workspaceFolder = workspaceService.getWorkspaceFolder(documentManager.activeTextEditor.document.uri); + if (workspaceFolder) { + return { configTarget: ConfigurationTarget.WorkspaceFolder, folderUri: workspaceFolder.uri }; + } } } - return undefined; } diff --git a/src/client/interpreter/index.ts b/src/client/interpreter/index.ts index f7c3c67c4e7d..db28b1a5df99 100644 --- a/src/client/interpreter/index.ts +++ b/src/client/interpreter/index.ts @@ -1,44 +1,36 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; -import { ConfigurationTarget, Disposable, StatusBarAlignment, Uri, window, workspace } from 'vscode'; +import { ConfigurationTarget, Disposable, Uri } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../common/application/types'; import { PythonSettings } from '../common/configSettings'; import { IPythonExecutionFactory } from '../common/process/types'; -import { IDisposableRegistry } from '../common/types'; +import { IConfigurationService, IDisposableRegistry } from '../common/types'; import * as utils from '../common/utils'; import { IServiceContainer } from '../ioc/types'; -import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService'; -import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory'; -import { IInterpreterLocatorService, IInterpreterService, IInterpreterVersionService, INTERPRETER_LOCATOR_SERVICE, InterpreterType, PythonInterpreter } from './contracts'; -import { InterpreterDisplay } from './display'; -import { getActiveWorkspaceUri } from './helpers'; -import { PythonInterpreterLocatorService } from './locators'; -import { VirtualEnvService } from './locators/services/virtualEnvService'; -import { IVirtualEnvironmentManager } from './virtualEnvs/types'; +import { IPythonPathUpdaterServiceManager } from './configuration/types'; +import { IInterpreterDisplay, IInterpreterHelper, IInterpreterLocatorService, IInterpreterService, IInterpreterVersionService, INTERPRETER_LOCATOR_SERVICE, InterpreterType, PythonInterpreter, WORKSPACE_VIRTUAL_ENV_SERVICE } from './contracts'; @injectable() export class InterpreterManager implements Disposable, IInterpreterService { - private display: InterpreterDisplay | null | undefined; - private interpreterProvider: PythonInterpreterLocatorService; - private pythonPathUpdaterService: PythonPathUpdaterService; - constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer) { - const virtualEnvMgr = serviceContainer.get(IVirtualEnvironmentManager); - const statusBar = window.createStatusBarItem(StatusBarAlignment.Left); - this.interpreterProvider = serviceContainer.get(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); - const versionService = serviceContainer.get(IInterpreterVersionService); - this.display = new InterpreterDisplay(statusBar, this, virtualEnvMgr, versionService); - this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), versionService); + private readonly interpreterProvider: IInterpreterLocatorService; + private readonly pythonPathUpdaterService: IPythonPathUpdaterServiceManager; + private readonly helper: IInterpreterHelper; + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.interpreterProvider = serviceContainer.get(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); + this.helper = serviceContainer.get(IInterpreterHelper); - const disposables = this.serviceContainer.get(IDisposableRegistry); - disposables.push(statusBar); - disposables.push(this.display!); + this.pythonPathUpdaterService = this.serviceContainer.get(IPythonPathUpdaterServiceManager); } - public async refresh() { - return this.display!.refresh(); + public async refresh(resource?: Uri) { + const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); + return interpreterDisplay.refresh(resource); } public initialize() { const disposables = this.serviceContainer.get(IDisposableRegistry); - disposables.push(window.onDidChangeActiveTextEditor(() => this.refresh())); - PythonSettings.getInstance().addListener('change', () => this.onConfigChanged()); + const documentManager = this.serviceContainer.get(IDocumentManager); + disposables.push(documentManager.onDidChangeActiveTextEditor((e) => this.refresh(e.document.uri))); + const configService = this.serviceContainer.get(IConfigurationService); + (configService.getSettings() as PythonSettings).addListener('change', this.onConfigChanged); } public getInterpreters(resource?: Uri) { return this.interpreterProvider.getInterpreters(resource); @@ -47,13 +39,11 @@ export class InterpreterManager implements Disposable, IInterpreterService { if (!this.shouldAutoSetInterpreter()) { return; } - const activeWorkspace = getActiveWorkspaceUri(); + const activeWorkspace = this.helper.getActiveWorkspaceUri(); if (!activeWorkspace) { return; } - const virtualEnvMgr = this.serviceContainer.get(IVirtualEnvironmentManager); - const versionService = this.serviceContainer.get(IInterpreterVersionService); - const virtualEnvInterpreterProvider = new VirtualEnvService([activeWorkspace.folderUri.fsPath], virtualEnvMgr, versionService, this.serviceContainer); + const virtualEnvInterpreterProvider = this.serviceContainer.get(IInterpreterLocatorService, WORKSPACE_VIRTUAL_ENV_SERVICE); const interpreters = await virtualEnvInterpreterProvider.getInterpreters(activeWorkspace.folderUri); const workspacePathUpper = activeWorkspace.folderUri.fsPath.toUpperCase(); @@ -73,8 +63,9 @@ export class InterpreterManager implements Disposable, IInterpreterService { } } public dispose(): void { - this.display = null; this.interpreterProvider.dispose(); + const configService = this.serviceContainer.get(IConfigurationService); + (configService.getSettings() as PythonSettings).removeListener('change', this.onConfigChanged); } public async getActiveInterpreter(resource?: Uri): Promise { @@ -100,11 +91,12 @@ export class InterpreterManager implements Disposable, IInterpreterService { }; } private shouldAutoSetInterpreter() { - const activeWorkspace = getActiveWorkspaceUri(); + const activeWorkspace = this.helper.getActiveWorkspaceUri(); if (!activeWorkspace) { return false; } - const pythonConfig = workspace.getConfiguration('python', activeWorkspace.folderUri); + const workspaceService = this.serviceContainer.get(IWorkspaceService); + const pythonConfig = workspaceService.getConfiguration('python', activeWorkspace.folderUri); const pythonPathInConfig = pythonConfig.inspect('pythonPath'); // If we have a value in user settings, then don't auto set the interpreter path. if (pythonPathInConfig && pythonPathInConfig!.globalValue !== undefined && pythonPathInConfig!.globalValue !== 'python') { @@ -118,10 +110,9 @@ export class InterpreterManager implements Disposable, IInterpreterService { } return false; } - private onConfigChanged() { - if (this.display) { - this.display!.refresh() - .catch(ex => console.error('Python Extension: display.refresh', ex)); - } + private onConfigChanged = () => { + const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); + interpreterDisplay.refresh() + .catch(ex => console.error('Python Extension: display.refresh', ex)); } } diff --git a/src/client/interpreter/locators/index.ts b/src/client/interpreter/locators/index.ts index e287cddbf8a9..3645e15cee14 100644 --- a/src/client/interpreter/locators/index.ts +++ b/src/client/interpreter/locators/index.ts @@ -10,12 +10,13 @@ import { CONDA_ENV_FILE_SERVICE, CONDA_ENV_SERVICE, CURRENT_PATH_SERVICE, + GLOBAL_VIRTUAL_ENV_SERVICE, IInterpreterLocatorService, InterpreterType, KNOWN_PATH_SERVICE, PythonInterpreter, - VIRTUAL_ENV_SERVICE, - WINDOWS_REGISTRY_SERVICE + WINDOWS_REGISTRY_SERVICE, + WORKSPACE_VIRTUAL_ENV_SERVICE } from '../contracts'; import { fixInterpreterDisplayName } from './helpers'; @@ -24,7 +25,7 @@ export class PythonInterpreterLocatorService implements IInterpreterLocatorServi private disposables: Disposable[] = []; private platform: IPlatformService; - constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer) { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { serviceContainer.get(IDisposableRegistry).push(this); this.platform = serviceContainer.get(IPlatformService); } @@ -67,7 +68,8 @@ export class PythonInterpreterLocatorService implements IInterpreterLocatorServi } locators.push(this.serviceContainer.get(IInterpreterLocatorService, CONDA_ENV_SERVICE)); locators.push(this.serviceContainer.get(IInterpreterLocatorService, CONDA_ENV_FILE_SERVICE)); - locators.push(this.serviceContainer.get(IInterpreterLocatorService, VIRTUAL_ENV_SERVICE)); + locators.push(this.serviceContainer.get(IInterpreterLocatorService, GLOBAL_VIRTUAL_ENV_SERVICE)); + locators.push(this.serviceContainer.get(IInterpreterLocatorService, WORKSPACE_VIRTUAL_ENV_SERVICE)); if (!this.platform.isWindows) { locators.push(this.serviceContainer.get(IInterpreterLocatorService, KNOWN_PATH_SERVICE)); diff --git a/src/client/interpreter/locators/services/baseVirtualEnvService.ts b/src/client/interpreter/locators/services/baseVirtualEnvService.ts new file mode 100644 index 000000000000..a060bf8502da --- /dev/null +++ b/src/client/interpreter/locators/services/baseVirtualEnvService.ts @@ -0,0 +1,88 @@ +import { injectable, unmanaged } from 'inversify'; +import * as _ from 'lodash'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IFileSystem, IPlatformService } from '../../../common/platform/types'; +import { IServiceContainer } from '../../../ioc/types'; +import { IInterpreterVersionService, InterpreterType, IVirtualEnvironmentsSearchPathProvider, PythonInterpreter } from '../../contracts'; +import { IVirtualEnvironmentManager } from '../../virtualEnvs/types'; +import { lookForInterpretersInDirectory } from '../helpers'; +import { CacheableLocatorService } from './cacheableLocatorService'; + +@injectable() +export class BaseVirtualEnvService extends CacheableLocatorService { + private readonly virtualEnvMgr: IVirtualEnvironmentManager; + private readonly versionProvider: IInterpreterVersionService; + private readonly fileSystem: IFileSystem; + public constructor(@unmanaged() private searchPathsProvider: IVirtualEnvironmentsSearchPathProvider, + @unmanaged() serviceContainer: IServiceContainer, + @unmanaged() name: string, + @unmanaged() cachePerWorkspace: boolean = false) { + super(name, serviceContainer, cachePerWorkspace); + this.virtualEnvMgr = serviceContainer.get(IVirtualEnvironmentManager); + this.versionProvider = serviceContainer.get(IInterpreterVersionService); + this.fileSystem = serviceContainer.get(IFileSystem); + } + // tslint:disable-next-line:no-empty + public dispose() { } + protected getInterpretersImplementation(resource?: Uri): Promise { + return this.suggestionsFromKnownVenvs(resource); + } + private async suggestionsFromKnownVenvs(resource?: Uri) { + const searchPaths = this.searchPathsProvider.getSearchPaths(resource); + return Promise.all(searchPaths.map(dir => this.lookForInterpretersInVenvs(dir))) + .then(listOfInterpreters => _.flatten(listOfInterpreters)); + } + private async lookForInterpretersInVenvs(pathToCheck: string) { + return this.fileSystem.getSubDirectoriesAsync(pathToCheck) + .then(subDirs => Promise.all(this.getProspectiveDirectoriesForLookup(subDirs))) + .then(dirs => dirs.filter(dir => dir.length > 0)) + .then(dirs => Promise.all(dirs.map(lookForInterpretersInDirectory))) + .then(pathsWithInterpreters => _.flatten(pathsWithInterpreters)) + .then(interpreters => Promise.all(interpreters.map(interpreter => this.getVirtualEnvDetails(interpreter)))) + .catch((err) => { + console.error('Python Extension (lookForInterpretersInVenvs):', err); + // Ignore exceptions. + return [] as PythonInterpreter[]; + }); + } + private getProspectiveDirectoriesForLookup(subDirs: string[]) { + const isWindows = this.serviceContainer.get(IPlatformService).isWindows; + const dirToLookFor = isWindows ? 'SCRIPTS' : 'bin'; + return subDirs.map(subDir => + this.fileSystem.getSubDirectoriesAsync(subDir) + .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 scriptOrBinDirs.length === 1 ? scriptOrBinDirs[0] : ''; + }) + .catch((err) => { + console.error('Python Extension (getProspectiveDirectoriesForLookup):', err); + // Ignore exceptions. + return ''; + })); + } + private async getVirtualEnvDetails(interpreter: string): Promise { + return Promise.all([ + this.versionProvider.getVersion(interpreter, path.basename(interpreter)), + this.virtualEnvMgr.detect(interpreter) + ]) + .then(([displayName, virtualEnv]) => { + const virtualEnvSuffix = virtualEnv ? virtualEnv.name : this.getVirtualEnvironmentRootDirectory(interpreter); + return { + displayName: `${displayName} (${virtualEnvSuffix})`.trim(), + path: interpreter, + type: virtualEnv ? virtualEnv.type : InterpreterType.Unknown + }; + }); + } + private getVirtualEnvironmentRootDirectory(interpreter: string) { + // Python interperters are always in a subdirectory of the environment folder. + return path.basename(path.dirname(path.dirname(interpreter))); + } +} diff --git a/src/client/interpreter/locators/services/cacheableLocatorService.ts b/src/client/interpreter/locators/services/cacheableLocatorService.ts index ffa7ed01c8de..ab4c0940f7cc 100644 --- a/src/client/interpreter/locators/services/cacheableLocatorService.ts +++ b/src/client/interpreter/locators/services/cacheableLocatorService.ts @@ -1,8 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { injectable } from 'inversify'; +// tslint:disable:no-any + +import { injectable, unmanaged } from 'inversify'; +import * as md5 from 'md5'; import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; import { createDeferred, Deferred } from '../../../common/helpers'; import { IPersistentStateFactory } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; @@ -10,50 +14,73 @@ import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; @injectable() export abstract class CacheableLocatorService implements IInterpreterLocatorService { - private getInterpretersPromise: Deferred; - private readonly cacheKey: string; - constructor(name: string, - protected readonly serviceContainer: IServiceContainer) { - this.cacheKey = `INTERPRETERS_CACHE_${name}`; + private readonly promisesPerResource = new Map>(); + private readonly cacheKeyPrefix: string; + constructor(@unmanaged() name: string, + @unmanaged() protected readonly serviceContainer: IServiceContainer, + @unmanaged() private cachePerWorkspace: boolean = false) { + this.cacheKeyPrefix = `INTERPRETERS_CACHE_${name}`; } public abstract dispose(); public async getInterpreters(resource?: Uri): Promise { - if (!this.getInterpretersPromise) { - this.getInterpretersPromise = createDeferred(); + const cacheKey = this.getCacheKey(resource); + let deferred = this.promisesPerResource.get(cacheKey); + if (!deferred) { + deferred = createDeferred(); + this.promisesPerResource.set(cacheKey, deferred); this.getInterpretersImplementation(resource) .then(async items => { - await this.cacheInterpreters(items); - this.getInterpretersPromise.resolve(items); + await this.cacheInterpreters(items, resource); + deferred!.resolve(items); }) - .catch(ex => this.getInterpretersPromise.reject(ex)); + .catch(ex => deferred!.reject(ex)); } - if (this.getInterpretersPromise.completed) { - return this.getInterpretersPromise.promise; + if (deferred.completed) { + return deferred.promise; } - const cachedInterpreters = this.getCachedInterpreters(); - return Array.isArray(cachedInterpreters) ? cachedInterpreters : this.getInterpretersPromise.promise; + const cachedInterpreters = this.getCachedInterpreters(resource); + return Array.isArray(cachedInterpreters) ? cachedInterpreters : deferred.promise; } protected abstract getInterpretersImplementation(resource?: Uri): Promise; - - private getCachedInterpreters() { + private createPersistenceStore(resource?: Uri) { + const cacheKey = this.getCacheKey(resource); const persistentFactory = this.serviceContainer.get(IPersistentStateFactory); - // tslint:disable-next-line:no-any - const globalPersistence = persistentFactory.createGlobalPersistentState(this.cacheKey, undefined as any); - if (!Array.isArray(globalPersistence.value)) { + if (this.cachePerWorkspace) { + return persistentFactory.createWorkspacePersistentState(cacheKey, undefined as any); + } else { + return persistentFactory.createGlobalPersistentState(cacheKey, undefined as any); + } + + } + private getCachedInterpreters(resource?: Uri) { + const persistence = this.createPersistenceStore(resource); + if (!Array.isArray(persistence.value)) { return; } - return globalPersistence.value.map(item => { + return persistence.value.map(item => { return { ...item, cachedEntry: true }; }); } - private async cacheInterpreters(interpreters: PythonInterpreter[]) { - const persistentFactory = this.serviceContainer.get(IPersistentStateFactory); - const globalPersistence = persistentFactory.createGlobalPersistentState(this.cacheKey, []); - await globalPersistence.updateValue(interpreters); + private async cacheInterpreters(interpreters: PythonInterpreter[], resource?: Uri) { + const persistence = this.createPersistenceStore(resource); + await persistence.updateValue(interpreters); + } + private getCacheKey(resource?: Uri) { + if (!resource || !this.cachePerWorkspace) { + return this.cacheKeyPrefix; + } + // Ensure we have separate caches per workspace where necessary.ÃŽ + const workspaceService = this.serviceContainer.get(IWorkspaceService); + if (!Array.isArray(workspaceService.workspaceFolders)) { + return this.cacheKeyPrefix; + } + + const workspace = workspaceService.getWorkspaceFolder(resource); + return workspace ? `${this.cacheKeyPrefix}:${md5(workspace.uri.fsPath)}` : this.cacheKeyPrefix; } } diff --git a/src/client/interpreter/locators/services/globalVirtualEnvService.ts b/src/client/interpreter/locators/services/globalVirtualEnvService.ts new file mode 100644 index 000000000000..22a3b4937e45 --- /dev/null +++ b/src/client/interpreter/locators/services/globalVirtualEnvService.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { Uri } from 'vscode'; +import { IPlatformService } from '../../../common/platform/types'; +import { IServiceContainer } from '../../../ioc/types'; +import { IVirtualEnvironmentsSearchPathProvider } from '../../contracts'; +import { BaseVirtualEnvService } from './baseVirtualEnvService'; + +// tslint:disable-next-line:no-require-imports no-var-requires +const untildify = require('untildify'); + +@injectable() +export class GlobalVirtualEnvService extends BaseVirtualEnvService { + public constructor( + @inject(IVirtualEnvironmentsSearchPathProvider) @named('global') globalVirtualEnvPathProvider: IVirtualEnvironmentsSearchPathProvider, + @inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(globalVirtualEnvPathProvider, serviceContainer, 'VirtualEnvService'); + } +} + +@injectable() +export class GlobalVirtualEnvironmentsSearchPathProvider implements IVirtualEnvironmentsSearchPathProvider { + public constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + + } + public getSearchPaths(_resource?: Uri): string[] { + const platformService = this.serviceContainer.get(IPlatformService); + if (platformService.isWindows) { + return []; + } else { + return ['/Envs', '/.virtualenvs', '/.pyenv', '/.pyenv/versions'] + .map(item => untildify(`~${item}`)); + } + } +} diff --git a/src/client/interpreter/locators/services/virtualEnvService.ts b/src/client/interpreter/locators/services/virtualEnvService.ts deleted file mode 100644 index d073c42d51ec..000000000000 --- a/src/client/interpreter/locators/services/virtualEnvService.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { inject, injectable } from 'inversify'; -import * as _ from 'lodash'; -import * as path from 'path'; -import { Uri, workspace } from 'vscode'; -import { fsReaddirAsync, IS_WINDOWS } from '../../../common/utils'; -import { IServiceContainer } from '../../../ioc/types'; -import { IInterpreterVersionService, IKnownSearchPathsForVirtualEnvironments, InterpreterType, PythonInterpreter } from '../../contracts'; -import { IVirtualEnvironmentManager } from '../../virtualEnvs/types'; -import { lookForInterpretersInDirectory } from '../helpers'; -import * as settings from './../../../common/configSettings'; -import { CacheableLocatorService } from './cacheableLocatorService'; - -// tslint:disable-next-line:no-require-imports no-var-requires -const untildify = require('untildify'); - -@injectable() -export class VirtualEnvService extends CacheableLocatorService { - public constructor( @inject(IKnownSearchPathsForVirtualEnvironments) private knownSearchPaths: string[], - @inject(IVirtualEnvironmentManager) private virtualEnvMgr: IVirtualEnvironmentManager, - @inject(IInterpreterVersionService) private versionProvider: IInterpreterVersionService, - @inject(IServiceContainer) serviceContainer: IServiceContainer) { - super('KnownPathsService', serviceContainer); - } - // tslint:disable-next-line:no-empty - public dispose() { } - protected getInterpretersImplementation(resource?: Uri): Promise { - return this.suggestionsFromKnownVenvs(); - } - private async suggestionsFromKnownVenvs() { - return Promise.all(this.knownSearchPaths.map(dir => this.lookForInterpretersInVenvs(dir))) - // tslint:disable-next-line:underscore-consistent-invocation - .then(listOfInterpreters => _.flatten(listOfInterpreters)); - } - private async lookForInterpretersInVenvs(pathToCheck: string) { - return fsReaddirAsync(pathToCheck) - .then(subDirs => Promise.all(this.getProspectiveDirectoriesForLookup(subDirs))) - .then(dirs => dirs.filter(dir => dir.length > 0)) - .then(dirs => Promise.all(dirs.map(lookForInterpretersInDirectory))) - // tslint:disable-next-line:underscore-consistent-invocation - .then(pathsWithInterpreters => _.flatten(pathsWithInterpreters)) - .then(interpreters => Promise.all(interpreters.map(interpreter => this.getVirtualEnvDetails(interpreter)))) - .catch((err) => { - console.error('Python Extension (lookForInterpretersInVenvs):', err); - // Ignore exceptions. - return [] as PythonInterpreter[]; - }); - } - private getProspectiveDirectoriesForLookup(subDirs: string[]) { - const dirToLookFor = IS_WINDOWS ? 'SCRIPTS' : 'BIN'; - return subDirs.map(subDir => fsReaddirAsync(subDir) - .then(dirs => { - const scriptOrBinDirs = dirs.filter(dir => { - const folderName = path.basename(dir); - return folderName.toUpperCase() === dirToLookFor; - }); - return scriptOrBinDirs.length === 1 ? scriptOrBinDirs[0] : ''; - }) - .catch((err) => { - console.error('Python Extension (getProspectiveDirectoriesForLookup):', err); - // Ignore exceptions. - return ''; - })); - - } - private async getVirtualEnvDetails(interpreter: string): Promise { - return Promise.all([ - this.versionProvider.getVersion(interpreter, path.basename(interpreter)), - this.virtualEnvMgr.detect(interpreter) - ]) - .then(([displayName, virtualEnv]) => { - const virtualEnvSuffix = virtualEnv ? virtualEnv.name : this.getVirtualEnvironmentRootDirectory(interpreter); - return { - displayName: `${displayName} (${virtualEnvSuffix})`.trim(), - path: interpreter, - type: virtualEnv ? virtualEnv.type : InterpreterType.Unknown - }; - }); - } - private getVirtualEnvironmentRootDirectory(interpreter: string) { - return path.basename(path.dirname(path.dirname(interpreter))); - } -} - -export function getKnownSearchPathsForVirtualEnvs(resource?: Uri): string[] { - const paths: string[] = []; - if (!IS_WINDOWS) { - const defaultPaths = ['/Envs', '/.virtualenvs', '/.pyenv', '/.pyenv/versions']; - defaultPaths.forEach(p => { - paths.push(untildify(`~${p}`)); - }); - } - const venvPath = settings.PythonSettings.getInstance(resource).venvPath; - if (venvPath) { - paths.push(untildify(venvPath)); - } - if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { - if (resource && workspace.workspaceFolders.length > 1) { - const wkspaceFolder = workspace.getWorkspaceFolder(resource); - if (wkspaceFolder) { - paths.push(wkspaceFolder.uri.fsPath); - } - } else { - paths.push(workspace.workspaceFolders[0].uri.fsPath); - } - } - return paths; -} diff --git a/src/client/interpreter/locators/services/workspaceVirtualEnvService.ts b/src/client/interpreter/locators/services/workspaceVirtualEnvService.ts new file mode 100644 index 000000000000..47cb2a803847 --- /dev/null +++ b/src/client/interpreter/locators/services/workspaceVirtualEnvService.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +// tslint:disable-next-line:no-require-imports +import untildify = require('untildify'); +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; +import { IConfigurationService } from '../../../common/types'; +import { IServiceContainer } from '../../../ioc/types'; +import { IVirtualEnvironmentsSearchPathProvider } from '../../contracts'; +import { BaseVirtualEnvService } from './baseVirtualEnvService'; + +@injectable() +export class WorkspaceVirtualEnvService extends BaseVirtualEnvService { + public constructor( + @inject(IVirtualEnvironmentsSearchPathProvider) @named('workspace') globalVirtualEnvPathProvider: IVirtualEnvironmentsSearchPathProvider, + @inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(globalVirtualEnvPathProvider, serviceContainer, 'WorkspaceVirtualEnvService', true); + } +} + +@injectable() +export class WorkspaceVirtualEnvironmentsSearchPathProvider implements IVirtualEnvironmentsSearchPathProvider { + public constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + + } + public getSearchPaths(resource?: Uri): string[] { + const configService = this.serviceContainer.get(IConfigurationService); + const paths: string[] = []; + const venvPath = configService.getSettings(resource).venvPath; + if (venvPath) { + paths.push(untildify(venvPath)); + } + const workspaceService = this.serviceContainer.get(IWorkspaceService); + if (Array.isArray(workspaceService.workspaceFolders) && workspaceService.workspaceFolders.length > 0) { + if (resource && workspaceService.workspaceFolders.length > 1) { + const wkspaceFolder = workspaceService.getWorkspaceFolder(resource); + if (wkspaceFolder) { + paths.push(wkspaceFolder.uri.fsPath); + } + } else { + paths.push(workspaceService.workspaceFolders[0].uri.fsPath); + } + } + return paths; + + } +} diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index de0776a73218..7c4c0cbb55c6 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -3,21 +3,32 @@ import { IsWindows } from '../common/types'; import { IServiceManager } from '../ioc/types'; +import { InterpreterSelector } from './configuration/interpreterSelector'; +import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService'; +import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory'; +import { IInterpreterSelector, IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager } from './configuration/types'; import { CONDA_ENV_FILE_SERVICE, CONDA_ENV_SERVICE, CURRENT_PATH_SERVICE, + GLOBAL_VIRTUAL_ENV_SERVICE, ICondaService, + IInterpreterDisplay, + IInterpreterHelper, IInterpreterLocatorService, IInterpreterService, IInterpreterVersionService, IKnownSearchPathsForInterpreters, - IKnownSearchPathsForVirtualEnvironments, INTERPRETER_LOCATOR_SERVICE, + IShebangCodeLensProvider, + IVirtualEnvironmentsSearchPathProvider, KNOWN_PATH_SERVICE, - VIRTUAL_ENV_SERVICE, - WINDOWS_REGISTRY_SERVICE + WINDOWS_REGISTRY_SERVICE, + WORKSPACE_VIRTUAL_ENV_SERVICE } from './contracts'; +import { InterpreterDisplay } from './display'; +import { ShebangCodeLensProvider } from './display/shebangCodeLensProvider'; +import { InterpreterHelper } from './helpers'; import { InterpreterManager } from './index'; import { InterpreterVersionService } from './interpreterVersion'; import { PythonInterpreterLocatorService } from './locators/index'; @@ -25,9 +36,10 @@ import { CondaEnvFileService } from './locators/services/condaEnvFileService'; import { CondaEnvService } from './locators/services/condaEnvService'; import { CondaService } from './locators/services/condaService'; import { CurrentPathService } from './locators/services/currentPathService'; +import { GlobalVirtualEnvironmentsSearchPathProvider, GlobalVirtualEnvService } from './locators/services/globalVirtualEnvService'; import { getKnownSearchPathsForInterpreters, KnownPathsService } from './locators/services/KnownPathsService'; -import { getKnownSearchPathsForVirtualEnvs, VirtualEnvService } from './locators/services/virtualEnvService'; import { WindowsRegistryService } from './locators/services/windowsRegistryService'; +import { WorkspaceVirtualEnvironmentsSearchPathProvider, WorkspaceVirtualEnvService } from './locators/services/workspaceVirtualEnvService'; import { VirtualEnvironmentManager } from './virtualEnvs/index'; import { IVirtualEnvironmentIdentifier, IVirtualEnvironmentManager } from './virtualEnvs/types'; import { VEnv } from './virtualEnvs/venv'; @@ -35,7 +47,8 @@ import { VirtualEnv } from './virtualEnvs/virtualEnv'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingletonInstance(IKnownSearchPathsForInterpreters, getKnownSearchPathsForInterpreters()); - serviceManager.addSingletonInstance(IKnownSearchPathsForVirtualEnvironments, getKnownSearchPathsForVirtualEnvs()); + serviceManager.addSingleton(IVirtualEnvironmentsSearchPathProvider, GlobalVirtualEnvironmentsSearchPathProvider, 'global'); + serviceManager.addSingleton(IVirtualEnvironmentsSearchPathProvider, WorkspaceVirtualEnvironmentsSearchPathProvider, 'workspace'); serviceManager.addSingleton(ICondaService, CondaService); serviceManager.addSingleton(IVirtualEnvironmentIdentifier, VirtualEnv); @@ -48,7 +61,8 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IInterpreterLocatorService, CondaEnvFileService, CONDA_ENV_FILE_SERVICE); serviceManager.addSingleton(IInterpreterLocatorService, CondaEnvService, CONDA_ENV_SERVICE); serviceManager.addSingleton(IInterpreterLocatorService, CurrentPathService, CURRENT_PATH_SERVICE); - serviceManager.addSingleton(IInterpreterLocatorService, VirtualEnvService, VIRTUAL_ENV_SERVICE); + serviceManager.addSingleton(IInterpreterLocatorService, GlobalVirtualEnvService, GLOBAL_VIRTUAL_ENV_SERVICE); + serviceManager.addSingleton(IInterpreterLocatorService, WorkspaceVirtualEnvService, WORKSPACE_VIRTUAL_ENV_SERVICE); const isWindows = serviceManager.get(IsWindows); if (isWindows) { @@ -57,4 +71,12 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IInterpreterLocatorService, KnownPathsService, KNOWN_PATH_SERVICE); } serviceManager.addSingleton(IInterpreterService, InterpreterManager); + serviceManager.addSingleton(IInterpreterDisplay, InterpreterDisplay); + + serviceManager.addSingleton(IPythonPathUpdaterServiceFactory, PythonPathUpdaterServiceFactory); + serviceManager.addSingleton(IPythonPathUpdaterServiceManager, PythonPathUpdaterService); + + serviceManager.addSingleton(IInterpreterSelector, InterpreterSelector); + serviceManager.addSingleton(IShebangCodeLensProvider, ShebangCodeLensProvider); + serviceManager.addSingleton(IInterpreterHelper, InterpreterHelper); } diff --git a/src/test/.vscode/settings.json b/src/test/.vscode/settings.json index a6a0df6d1f21..d0a948e74069 100644 --- a/src/test/.vscode/settings.json +++ b/src/test/.vscode/settings.json @@ -22,6 +22,5 @@ "python.linting.pylamaEnabled": false, "python.linting.mypyEnabled": false, "python.formatting.provider": "yapf", - "python.pythonPath": "python", "python.linting.pylintUseMinimalCheckers": false } \ No newline at end of file diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index 4574a4925c42..4af691063219 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -37,12 +37,13 @@ suite('Installer', () => { }); teardown(async () => { ioc.dispose(); - closeActiveWindows(); + await closeActiveWindows(); }); function initializeDI() { ioc = new UnitTestIocContainer(); ioc.registerUnitTestTypes(); + ioc.registerFileSystemTypes(); ioc.registerVariableTypes(); ioc.registerLinterTypes(); ioc.registerFormatterTypes(); diff --git a/src/test/common/process/execFactory.test.ts b/src/test/common/process/execFactory.test.ts new file mode 100644 index 000000000000..3d0b2404a076 --- /dev/null +++ b/src/test/common/process/execFactory.test.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { PythonExecutionFactory } from '../../../client/common/process/pythonExecutionFactory'; +import { IProcessService } from '../../../client/common/process/types'; +import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; +import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; +import { IServiceContainer } from '../../../client/ioc/types'; + +// tslint:disable-next-line:max-func-body-length +suite('PythonExecutableService', () => { + let serviceContainer: TypeMoq.IMock; + let configService: TypeMoq.IMock; + let procService: TypeMoq.IMock; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + const envVarsProvider = TypeMoq.Mock.ofType(); + procService = TypeMoq.Mock.ofType(); + configService = TypeMoq.Mock.ofType(); + const fileSystem = TypeMoq.Mock.ofType(); + fileSystem.setup(f => f.fileExistsAsync(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider))).returns(() => envVarsProvider.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProcessService))).returns(() => procService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configService.object); + envVarsProvider.setup(v => v.getEnvironmentVariables(TypeMoq.It.isAny())).returns(() => Promise.resolve({})); + + }); + test('Ensure resource is used when getting configuration service settings (undefined resource)', async () => { + const pythonPath = `Python_Path_${new Date().toString()}`; + const pythonVersion = `Python_Version_${new Date().toString()}`; + const pythonSettings = TypeMoq.Mock.ofType(); + pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); + configService.setup(c => c.getSettings(TypeMoq.It.isValue(undefined))).returns(() => pythonSettings.object); + procService.setup(p => p.exec(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: pythonVersion })); + + const factory = await new PythonExecutionFactory(serviceContainer.object).create(); + const version = await factory.getVersion(); + + expect(version).to.be.equal(pythonVersion); + }); + test('Ensure resource is used when getting configuration service settings (defined resource)', async () => { + const resource = Uri.file('abc'); + const pythonPath = `Python_Path_${new Date().toString()}`; + const pythonVersion = `Python_Version_${new Date().toString()}`; + const pythonSettings = TypeMoq.Mock.ofType(); + pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); + configService.setup(c => c.getSettings(TypeMoq.It.isValue(resource))).returns(() => pythonSettings.object); + procService.setup(p => p.exec(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: pythonVersion })); + + const factory = await new PythonExecutionFactory(serviceContainer.object).create(resource); + const version = await factory.getVersion(); + + expect(version).to.be.equal(pythonVersion); + }); +}); diff --git a/src/test/common/process/pythonProc.simple.multiroot.test.ts b/src/test/common/process/pythonProc.simple.multiroot.test.ts index 490a941a3655..1cbabdb6736f 100644 --- a/src/test/common/process/pythonProc.simple.multiroot.test.ts +++ b/src/test/common/process/pythonProc.simple.multiroot.test.ts @@ -10,7 +10,10 @@ import * as path from 'path'; import { ConfigurationTarget, Disposable, Uri } from 'vscode'; import { PythonSettings } from '../../../client/common/configSettings'; import { ConfigurationService } from '../../../client/common/configuration/service'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; import { PathUtils } from '../../../client/common/platform/pathUtils'; +import { PlatformService } from '../../../client/common/platform/platformService'; +import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; import { CurrentProcess } from '../../../client/common/process/currentProcess'; import { registerTypes as processRegisterTypes } from '../../../client/common/process/serviceRegistry'; import { IPythonExecutionFactory, StdErrError } from '../../../client/common/process/types'; @@ -57,6 +60,8 @@ suite('PythonExecutableService', () => { serviceManager.addSingleton(IPathUtils, PathUtils); serviceManager.addSingleton(ICurrentProcess, CurrentProcess); serviceManager.addSingleton(IConfigurationService, ConfigurationService); + serviceManager.addSingleton(IPlatformService, PlatformService); + serviceManager.addSingleton(IFileSystem, FileSystem); processRegisterTypes(serviceManager); variablesRegisterTypes(serviceManager); diff --git a/src/test/format/extension.formatOnSave.test.ts b/src/test/format/extension.formatOnSave.test.ts index d6bbd442bc1b..cc9b50f7862d 100644 --- a/src/test/format/extension.formatOnSave.test.ts +++ b/src/test/format/extension.formatOnSave.test.ts @@ -38,6 +38,7 @@ suite('Formating On Save', () => { function initializeDI() { ioc = new UnitTestIocContainer(); ioc.registerFormatterTypes(); + ioc.registerFileSystemTypes(); ioc.registerProcessTypes(); ioc.registerVariableTypes(); ioc.registerMockProcess(); diff --git a/src/test/interpreters/display.multiroot.test.ts b/src/test/interpreters/display.multiroot.test.ts deleted file mode 100644 index 3686b7239b81..000000000000 --- a/src/test/interpreters/display.multiroot.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as assert from 'assert'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Uri, window, workspace } from 'vscode'; -import { PythonSettings } from '../../client/common/configSettings'; -import { IInterpreterService } from '../../client/interpreter/contracts'; -import { InterpreterDisplay } from '../../client/interpreter/display'; -import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; -import { clearPythonPathInWorkspaceFolder } from '../common'; -import { closeActiveWindows, initialize, initializePython, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; -import { MockStatusBarItem } from '../mockClasses'; -import { UnitTestIocContainer } from '../unittests/serviceRegistry'; -import { MockInterpreterVersionProvider } from './mocks'; - -const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); -const workspace3Uri = Uri.file(path.join(multirootPath, 'workspace3')); -const fileToOpen = path.join(workspace3Uri.fsPath, 'file.py'); - -// tslint:disable-next-line:max-func-body-length -suite('Multiroot Interpreters Display', () => { - let ioc: UnitTestIocContainer; - suiteSetup(async function () { - if (!IS_MULTI_ROOT_TEST) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - } - await initialize(); - }); - setup(async () => { - await initializeTest(); - initializeDI(); - }); - suiteTeardown(initializePython); - teardown(async () => { - ioc.dispose(); - await clearPythonPathInWorkspaceFolder(fileToOpen); - await initialize(); - await closeActiveWindows(); - }); - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerVariableTypes(); - ioc.registerProcessTypes(); - } - - test('Must get display name from workspace folder interpreter and not from interpreter in workspace', async () => { - const settings = workspace.getConfiguration('python', Uri.file(fileToOpen)); - const pythonPath = fileToOpen; - await settings.update('pythonPath', pythonPath, ConfigurationTarget.WorkspaceFolder); - PythonSettings.dispose(); - - const document = await workspace.openTextDocument(fileToOpen); - await window.showTextDocument(document); - - const statusBar = new MockStatusBarItem(); - const provider = TypeMoq.Mock.ofType(); - provider.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); - provider.setup(p => p.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); - const displayName = `${path.basename(pythonPath)} [Environment]`; - const displayNameProvider = new MockInterpreterVersionProvider(displayName); - const display = new InterpreterDisplay(statusBar, provider.object, new VirtualEnvironmentManager([]), displayNameProvider); - await display.refresh(); - - assert.equal(statusBar.text, displayName, 'Incorrect display name'); - }); -}); diff --git a/src/test/interpreters/display.test.ts b/src/test/interpreters/display.test.ts index 27f2e80648b0..b5bd44751919 100644 --- a/src/test/interpreters/display.test.ts +++ b/src/test/interpreters/display.test.ts @@ -1,164 +1,202 @@ -import * as assert from 'assert'; -import * as child_process from 'child_process'; +import { expect } from 'chai'; import { EOL } from 'os'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Uri, window, workspace } from 'vscode'; -import { PythonSettings } from '../../client/common/configSettings'; -import { IInterpreterService, InterpreterType } from '../../client/interpreter/contracts'; +import { ConfigurationTarget, Disposable, StatusBarAlignment, StatusBarItem, Uri, WorkspaceFolder } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; +import { IFileSystem } from '../../client/common/platform/types'; +import { IConfigurationService, IDisposableRegistry, IPythonSettings } from '../../client/common/types'; +import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService, IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { InterpreterDisplay } from '../../client/interpreter/display'; -import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers'; -import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; -import { clearPythonPathInWorkspaceFolder, rootWorkspaceUri, updateSetting } from '../common'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; -import { MockStatusBarItem } from '../mockClasses'; -import { UnitTestIocContainer } from '../unittests/serviceRegistry'; -import { MockInterpreterVersionProvider } from './mocks'; -import { MockVirtualEnv } from './mocks'; - -const fileInNonRootWorkspace = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); +import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; +import { IServiceContainer } from '../../client/ioc/types'; // tslint:disable-next-line:max-func-body-length suite('Interpreters Display', () => { - let ioc: UnitTestIocContainer; - const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; - suiteSetup(initialize); - setup(async () => { - initializeDI(); - await initializeTest(); - if (IS_MULTI_ROOT_TEST) { - await initializeMultiRoot(); - } - }); - teardown(async () => { - ioc.dispose(); - await clearPythonPathInWorkspaceFolder(fileInNonRootWorkspace); - await initialize(); - await closeActiveWindows(); + let applicationShell: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + let serviceContainer: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + let virtualEnvMgr: TypeMoq.IMock; + let versionProvider: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + let disposableRegistry: Disposable[]; + let statusBar: TypeMoq.IMock; + let pythonSettings: TypeMoq.IMock; + let configurationService: TypeMoq.IMock; + let interpreterDisplay: IInterpreterDisplay; + let interpreterHelper: TypeMoq.IMock; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); + applicationShell = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + virtualEnvMgr = TypeMoq.Mock.ofType(); + versionProvider = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); + interpreterHelper = TypeMoq.Mock.ofType(); + disposableRegistry = []; + statusBar = TypeMoq.Mock.ofType(); + pythonSettings = TypeMoq.Mock.ofType(); + configurationService = TypeMoq.Mock.ofType(); + + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => applicationShell.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterService))).returns(() => interpreterService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IVirtualEnvironmentManager))).returns(() => virtualEnvMgr.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterVersionService))).returns(() => versionProvider.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => disposableRegistry); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configurationService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterHelper))).returns(() => interpreterHelper.object); + + applicationShell.setup(a => a.createStatusBarItem(TypeMoq.It.isValue(StatusBarAlignment.Left), TypeMoq.It.isValue(undefined))).returns(() => statusBar.object); + + interpreterDisplay = new InterpreterDisplay(serviceContainer.object); }); - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerVariableTypes(); - ioc.registerProcessTypes(); + function setupWorkspaceFolder(resource: Uri, workspaceFolder?: Uri) { + if (workspaceFolder) { + const mockFolder = TypeMoq.Mock.ofType(); + mockFolder.setup(w => w.uri).returns(() => workspaceFolder); + workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(resource))).returns(() => mockFolder.object); + } else { + workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(resource))).returns(() => undefined); + } } - test('Must have command name', () => { - const statusBar = new MockStatusBarItem(); - const displayNameProvider = new MockInterpreterVersionProvider(''); - const interpreterService = TypeMoq.Mock.ofType(); - interpreterService.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); - // tslint:disable-next-line:no-unused-expression - new InterpreterDisplay(statusBar, interpreterService.object, new VirtualEnvironmentManager([]), displayNameProvider); - assert.equal(statusBar.command, 'python.setInterpreter', 'Incorrect command name'); + test('Sattusbar must be created and have command name initialized', () => { + statusBar.verify(s => s.command = TypeMoq.It.isValue('python.setInterpreter'), TypeMoq.Times.once()); + expect(disposableRegistry).to.be.lengthOf.above(0); + expect(disposableRegistry).contain(statusBar.object); }); - test('Must get display name from interpreter itself', async () => { - const statusBar = new MockStatusBarItem(); - const interpreterService = TypeMoq.Mock.ofType(); - interpreterService.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); - const displayName = 'Mock Display Name'; - const displayNameProvider = new MockInterpreterVersionProvider(displayName); - const display = new InterpreterDisplay(statusBar, interpreterService.object, new VirtualEnvironmentManager([]), displayNameProvider); - await display.refresh(); - - assert.equal(statusBar.text, displayName, 'Incorrect display name'); + test('Display name and tooltip must come from interpreter info', async () => { + const resource = Uri.file('x'); + const workspaceFolder = Uri.file('workspace'); + const activeInterpreter: PythonInterpreter = { + displayName: 'Dummy_Display_Name', + type: InterpreterType.Unknown, + path: path.join('user', 'development', 'env', 'bin', 'python') + }; + setupWorkspaceFolder(resource, workspaceFolder); + interpreterService.setup(i => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve([])); + interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve(activeInterpreter)); + + await interpreterDisplay.refresh(resource); + + statusBar.verify(s => s.text = TypeMoq.It.isValue(activeInterpreter.displayName)!, TypeMoq.Times.once()); + statusBar.verify(s => s.tooltip = TypeMoq.It.isValue(activeInterpreter.path)!, TypeMoq.Times.once()); }); - test('Must suffix display name with name of interpreter', async () => { - const statusBar = new MockStatusBarItem(); - const interpreterService = TypeMoq.Mock.ofType(); - interpreterService.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); - const env1 = new MockVirtualEnv(false, 'Mock 1'); - const env2 = new MockVirtualEnv(true, 'Mock 2'); - const env3 = new MockVirtualEnv(true, 'Mock 3'); - const displayName = 'Mock Display Name'; - const displayNameProvider = new MockInterpreterVersionProvider(displayName); - const display = new InterpreterDisplay(statusBar, interpreterService.object, new VirtualEnvironmentManager([env1, env2, env3]), displayNameProvider); - await display.refresh(); - assert.equal(statusBar.text, `${displayName} (${env2.name})`, 'Incorrect display name'); + test('Display name and tooltip must include company display name from interpreter info', async () => { + const resource = Uri.file('x'); + const workspaceFolder = Uri.file('workspace'); + const activeInterpreter: PythonInterpreter = { + displayName: 'Dummy_Display_Name', + type: InterpreterType.Unknown, + companyDisplayName: 'Company Name', + path: path.join('user', 'development', 'env', 'bin', 'python') + }; + setupWorkspaceFolder(resource, workspaceFolder); + interpreterService.setup(i => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve([])); + interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve(activeInterpreter)); + const expectedTooltip = `${activeInterpreter.path}${EOL}${activeInterpreter.companyDisplayName}`; + + await interpreterDisplay.refresh(resource); + + statusBar.verify(s => s.text = TypeMoq.It.isValue(activeInterpreter.displayName)!, TypeMoq.Times.once()); + statusBar.verify(s => s.tooltip = TypeMoq.It.isValue(expectedTooltip)!, TypeMoq.Times.once()); }); - test('Must display default \'Display name\' for unknown interpreter', async () => { - const statusBar = new MockStatusBarItem(); - const interpreterService = TypeMoq.Mock.ofType(); - interpreterService.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); - const displayName = 'Mock Display Name'; - const displayNameProvider = new MockInterpreterVersionProvider(displayName, true); - const display = new InterpreterDisplay(statusBar, interpreterService.object, new VirtualEnvironmentManager([]), displayNameProvider); - // Change interpreter to an invalid value - const pythonPath = 'UnknownInterpreter'; - await updateSetting('pythonPath', pythonPath, rootWorkspaceUri, configTarget); - await display.refresh(); + test('If interpreter is not idenfied then tooltip should point to python Path', async () => { + const resource = Uri.file('x'); + const pythonPath = path.join('user', 'development', 'env', 'bin', 'python'); + const workspaceFolder = Uri.file('workspace'); + setupWorkspaceFolder(resource, workspaceFolder); + interpreterService.setup(i => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve([])); + interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve(undefined)); + configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); + virtualEnvMgr.setup(v => v.detect(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(undefined)); + + await interpreterDisplay.refresh(resource); + statusBar.verify(s => s.tooltip = TypeMoq.It.isValue(pythonPath), TypeMoq.Times.once()); + }); + test('If interpreter file does not exist then update status bar accordingly', async () => { + const resource = Uri.file('x'); + const pythonPath = path.join('user', 'development', 'env', 'bin', 'python'); + const workspaceFolder = Uri.file('workspace'); + setupWorkspaceFolder(resource, workspaceFolder); + // tslint:disable-next-line:no-any + interpreterService.setup(i => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve([{} as any])); + interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve(undefined)); + configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); + fileSystem.setup(f => f.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(false)); const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; - assert.equal(statusBar.text, defaultDisplayName, 'Incorrect display name'); + versionProvider.setup(v => v.getVersion(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())).returns(() => Promise.resolve(defaultDisplayName)); + virtualEnvMgr.setup(v => v.detect(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(undefined)); + + await interpreterDisplay.refresh(resource); + + statusBar.verify(s => s.color = TypeMoq.It.isValue('yellow'), TypeMoq.Times.once()); + statusBar.verify(s => s.text = TypeMoq.It.isValue('$(alert) Select Python Environment'), TypeMoq.Times.once()); }); - test('Must get display name from a list of interpreters', async () => { - const pythonPath = await new Promise(resolve => { - child_process.execFile(PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { - resolve(getFirstNonEmptyLineFromMultilineString(stdout)); - }); - }).then(value => value.length === 0 ? PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath : value); - const statusBar = new MockStatusBarItem(); - const interpreters = [ - { displayName: 'One', path: 'c:/path1/one.exe', type: InterpreterType.VirtualEnv }, - { displayName: 'Two', path: pythonPath, type: InterpreterType.VirtualEnv }, - { displayName: 'Three', path: 'c:/path3/three.exe', type: InterpreterType.VirtualEnv } - ]; - const interpreterService = TypeMoq.Mock.ofType(); - interpreterService.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(interpreters)); - interpreterService.setup(p => p.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => Promise.resolve(interpreters[1])); - const displayName = 'Mock Display Name'; - const displayNameProvider = new MockInterpreterVersionProvider(displayName, true); - const display = new InterpreterDisplay(statusBar, interpreterService.object, new VirtualEnvironmentManager([]), displayNameProvider); - await display.refresh(); - - assert.equal(statusBar.text, interpreters[1].displayName, 'Incorrect display name'); + test('Suffix display name with the virtual env name', async () => { + const resource = Uri.file('x'); + const pythonPath = path.join('user', 'development', 'env', 'bin', 'python'); + const workspaceFolder = Uri.file('workspace'); + setupWorkspaceFolder(resource, workspaceFolder); + // tslint:disable-next-line:no-any + interpreterService.setup(i => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve([{} as any])); + interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve(undefined)); + configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); + fileSystem.setup(f => f.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; + versionProvider.setup(v => v.getVersion(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())).returns(() => Promise.resolve(defaultDisplayName)); + // tslint:disable-next-line:no-any + virtualEnvMgr.setup(v => v.detect(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve({ name: 'Mock Env Name' } as any)); + const expectedText = `${defaultDisplayName} (Mock Env Name)`; + + await interpreterDisplay.refresh(resource); + + statusBar.verify(s => s.text = TypeMoq.It.isValue(expectedText), TypeMoq.Times.once()); }); - test('Must suffix tooltip with the companyDisplayName of interpreter', async () => { - const pythonPath = await new Promise(resolve => { - child_process.execFile(PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { - resolve(getFirstNonEmptyLineFromMultilineString(stdout)); - }); - }).then(value => value.length === 0 ? PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath : value); - - const statusBar = new MockStatusBarItem(); - const interpreters = [ - { displayName: 'One', path: 'c:/path1/one.exe', companyDisplayName: 'One 1', type: InterpreterType.VirtualEnv }, - { displayName: 'Two', path: pythonPath, companyDisplayName: 'Two 2', type: InterpreterType.VirtualEnv }, - { displayName: 'Three', path: 'c:/path3/three.exe', companyDisplayName: 'Three 3', type: InterpreterType.VirtualEnv } - ]; - const interpreterService = TypeMoq.Mock.ofType(); - interpreterService.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(interpreters)); - interpreterService.setup(p => p.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => Promise.resolve(interpreters[1])); - const displayNameProvider = new MockInterpreterVersionProvider(''); - const display = new InterpreterDisplay(statusBar, interpreterService.object, new VirtualEnvironmentManager([]), displayNameProvider); - await display.refresh(); - - assert.equal(statusBar.text, interpreters[1].displayName, 'Incorrect display name'); - assert.equal(statusBar.tooltip, `${pythonPath}${EOL}${interpreters[1].companyDisplayName}`, 'Incorrect tooltip'); + test('Use version of interpreter instead of a default interpreter name', async () => { + const resource = Uri.file('x'); + const pythonPath = path.join('user', 'development', 'env', 'bin', 'python'); + const workspaceFolder = Uri.file('workspace'); + setupWorkspaceFolder(resource, workspaceFolder); + // tslint:disable-next-line:no-any + interpreterService.setup(i => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve([{} as any])); + interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve(undefined)); + configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); + fileSystem.setup(f => f.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + const displayName = 'Version from Interperter'; + versionProvider.setup(v => v.getVersion(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())).returns(() => Promise.resolve(displayName)); + // tslint:disable-next-line:no-any + virtualEnvMgr.setup(v => v.detect(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(undefined)); + + await interpreterDisplay.refresh(resource); + + statusBar.verify(s => s.text = TypeMoq.It.isValue(displayName), TypeMoq.Times.once()); }); - test('Will update status prompting user to select an interpreter', async () => { - const statusBar = new MockStatusBarItem(); - const interpreters = [ - { displayName: 'One', path: 'c:/path1/one.exe', companyDisplayName: 'One 1', type: InterpreterType.VirtualEnv }, - { displayName: 'Two', path: 'c:/asdf', companyDisplayName: 'Two 2', type: InterpreterType.VirtualEnv }, - { displayName: 'Three', path: 'c:/path3/three.exe', companyDisplayName: 'Three 3', type: InterpreterType.VirtualEnv } - ]; - const interpreterService = TypeMoq.Mock.ofType(); - interpreterService.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(interpreters)); - const displayNameProvider = new MockInterpreterVersionProvider('', true); - const display = new InterpreterDisplay(statusBar, interpreterService.object, new VirtualEnvironmentManager([]), displayNameProvider); - // Change interpreter to an invalid value - const pythonPath = 'UnknownInterpreter'; - await updateSetting('pythonPath', pythonPath, rootWorkspaceUri, configTarget); - await display.refresh(); - - assert.equal(statusBar.text, '$(alert) Select Python Environment', 'Incorrect display name'); + test('Ensure we try to identify the active workspace when a resource is not provided ', async () => { + const workspaceFolder = Uri.file('x'); + const resource = workspaceFolder; + const activeInterpreter: PythonInterpreter = { + displayName: 'Dummy_Display_Name', + type: InterpreterType.Unknown, + companyDisplayName: 'Company Name', + path: path.join('user', 'development', 'env', 'bin', 'python') + }; + interpreterService.setup(i => i.getInterpreters(TypeMoq.It.isValue(resource))).returns(() => Promise.resolve([])); + interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(resource))).returns(() => Promise.resolve(activeInterpreter)); + const expectedTooltip = `${activeInterpreter.path}${EOL}${activeInterpreter.companyDisplayName}`; + interpreterHelper.setup(i => i.getActiveWorkspaceUri()).returns(() => { return { folderUri: workspaceFolder, configTarget: ConfigurationTarget.Workspace }; }); + + await interpreterDisplay.refresh(); + + statusBar.verify(s => s.text = TypeMoq.It.isValue(activeInterpreter.displayName)!, TypeMoq.Times.once()); + statusBar.verify(s => s.tooltip = TypeMoq.It.isValue(expectedTooltip)!, TypeMoq.Times.once()); }); - async function initializeMultiRoot() { - // For multiroot environments, we need a file open to determine the best interpreter that needs to be displayed - await openDummyFile(); - } - async function openDummyFile() { - const document = await workspace.openTextDocument(fileInNonRootWorkspace); - await window.showTextDocument(document); - } }); diff --git a/src/test/interpreters/helper.test.ts b/src/test/interpreters/helper.test.ts new file mode 100644 index 000000000000..ecab21922e6c --- /dev/null +++ b/src/test/interpreters/helper.test.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, TextDocument, TextEditor, Uri } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import { InterpreterHelper } from '../../client/interpreter/helpers'; +import { IServiceContainer } from '../../client/ioc/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Interpreters Display Helper', () => { + let documentManager: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + let serviceContainer: TypeMoq.IMock; + let helper: InterpreterHelper; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); + documentManager = TypeMoq.Mock.ofType(); + + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDocumentManager))).returns(() => documentManager.object); + + helper = new InterpreterHelper(serviceContainer.object); + }); + test('getActiveWorkspaceUri should return undefined if there are no workspaces', () => { + workspaceService.setup(w => w.workspaceFolders).returns(() => []); + + const workspace = helper.getActiveWorkspaceUri(); + expect(workspace).to.be.equal(undefined, 'incorrect value'); + }); + test('getActiveWorkspaceUri should return the workspace if there is only one', () => { + const folderUri = Uri.file('abc'); + // tslint:disable-next-line:no-any + workspaceService.setup(w => w.workspaceFolders).returns(() => [{ uri: folderUri } as any]); + + const workspace = helper.getActiveWorkspaceUri(); + expect(workspace).to.be.not.equal(undefined, 'incorrect value'); + expect(workspace!.folderUri).to.be.equal(folderUri); + expect(workspace!.configTarget).to.be.equal(ConfigurationTarget.Workspace); + }); + test('getActiveWorkspaceUri should return undefined if we no active editor and have more than one workspace folder', () => { + const folderUri = Uri.file('abc'); + // tslint:disable-next-line:no-any + workspaceService.setup(w => w.workspaceFolders).returns(() => [{ uri: folderUri } as any, undefined as any]); + documentManager.setup(d => d.activeTextEditor).returns(() => undefined); + + const workspace = helper.getActiveWorkspaceUri(); + expect(workspace).to.be.equal(undefined, 'incorrect value'); + }); + test('getActiveWorkspaceUri should return undefined of the active editor does not belong to a workspace and if we have more than one workspace folder', () => { + const folderUri = Uri.file('abc'); + const documentUri = Uri.file('file'); + // tslint:disable-next-line:no-any + workspaceService.setup(w => w.workspaceFolders).returns(() => [{ uri: folderUri } as any, undefined as any]); + const textEditor = TypeMoq.Mock.ofType(); + const document = TypeMoq.Mock.ofType(); + textEditor.setup(t => t.document).returns(() => document.object); + document.setup(d => d.uri).returns(() => documentUri); + documentManager.setup(d => d.activeTextEditor).returns(() => textEditor.object); + workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(documentUri))).returns(() => undefined); + + const workspace = helper.getActiveWorkspaceUri(); + expect(workspace).to.be.equal(undefined, 'incorrect value'); + }); + test('getActiveWorkspaceUri should return workspace folder of the active editor if belongs to a workspace and if we have more than one workspace folder', () => { + const folderUri = Uri.file('abc'); + const documentWorkspaceFolderUri = Uri.file('file.abc'); + const documentUri = Uri.file('file'); + // tslint:disable-next-line:no-any + workspaceService.setup(w => w.workspaceFolders).returns(() => [{ uri: folderUri } as any, undefined as any]); + const textEditor = TypeMoq.Mock.ofType(); + const document = TypeMoq.Mock.ofType(); + textEditor.setup(t => t.document).returns(() => document.object); + document.setup(d => d.uri).returns(() => documentUri); + documentManager.setup(d => d.activeTextEditor).returns(() => textEditor.object); + // tslint:disable-next-line:no-any + workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(documentUri))).returns(() => { return { uri: documentWorkspaceFolderUri } as any; }); + + const workspace = helper.getActiveWorkspaceUri(); + expect(workspace).to.be.not.equal(undefined, 'incorrect value'); + expect(workspace!.folderUri).to.be.equal(documentWorkspaceFolderUri); + expect(workspace!.configTarget).to.be.equal(ConfigurationTarget.WorkspaceFolder); + }); +}); diff --git a/src/test/interpreters/pythonPathUpdater.multiroot.test.ts b/src/test/interpreters/pythonPathUpdater.multiroot.test.ts deleted file mode 100644 index c79058b37c6b..000000000000 --- a/src/test/interpreters/pythonPathUpdater.multiroot.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import * as assert from 'assert'; -import * as path from 'path'; -import { ConfigurationTarget, Uri, workspace } from 'vscode'; -import { PythonSettings } from '../../client/common/configSettings'; -import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; -import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; -import { WorkspaceFolderPythonPathUpdaterService } from '../../client/interpreter/configuration/services/workspaceFolderUpdaterService'; -import { WorkspacePythonPathUpdaterService } from '../../client/interpreter/configuration/services/workspaceUpdaterService'; -import { IInterpreterVersionService } from '../../client/interpreter/contracts'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; -import { UnitTestIocContainer } from '../unittests/serviceRegistry'; - -const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); -const workspace3Uri = Uri.file(path.join(multirootPath, 'workspace3')); - -// tslint:disable-next-line:max-func-body-length -suite('Multiroot Python Path Settings Updater', () => { - let ioc: UnitTestIocContainer; - suiteSetup(async function () { - if (!IS_MULTI_ROOT_TEST) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - } - await initialize(); - }); - setup(async () => { - await initializeTest(); - initializeDI(); - }); - suiteTeardown(async () => { - await closeActiveWindows(); - await initializeTest(); - }); - teardown(async () => { - await closeActiveWindows(); - await initializeTest(); - ioc.dispose(); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerProcessTypes(); - ioc.registerVariableTypes(); - ioc.registerInterpreterTypes(); - } - - test('Updating Workspace Folder Python Path should work', async () => { - const workspaceUri = workspace3Uri; - const workspaceUpdater = new WorkspaceFolderPythonPathUpdaterService(workspace.getWorkspaceFolder(workspaceUri)!.uri); - const pythonPath = `xWorkspacePythonPath${new Date().getMilliseconds()}`; - await workspaceUpdater.updatePythonPath(pythonPath); - const folderValue = workspace.getConfiguration('python', workspace3Uri).inspect('pythonPath')!.workspaceFolderValue!; - assert.equal(folderValue, pythonPath, 'Workspace Python Path not updated'); - }); - - test('Updating Workspace Folder Python Path when Python Path is in a sub directory of Workspace Folder', async () => { - const workspaceUri = workspace3Uri; - const workspaceFolder = workspace.getWorkspaceFolder(workspaceUri)!.uri; - const workspaceUpdater = new WorkspaceFolderPythonPathUpdaterService(workspaceFolder); - - // Python path within a sub directory of the workspace folder. - const pythonExec = path.join('virtual Envs', 'env1', `xWorkspacePythonPath${new Date().getMilliseconds()}`); - - // Expected path to python executable where ${workspaceFolder} token represents the fully qualified path to executable. - // tslint:disable-next-line:no-invalid-template-strings - const rawPythonPath = path.join('${workspaceFolder}', pythonExec); - - // Fully qualified path to the python executable. - const pythonPath = path.join(workspaceFolder.fsPath, pythonExec); - await workspaceUpdater.updatePythonPath(pythonPath); - PythonSettings.dispose(); - - const folderValue = workspace.getConfiguration('python', workspace3Uri).inspect('pythonPath')!.workspaceFolderValue!; - assert.equal(folderValue, rawPythonPath, 'Raw Workspace Python Path not updated with a path relative to workspace folder'); - const resolvedPythonPath = PythonSettings.getInstance(workspaceUri).pythonPath; - assert.equal(resolvedPythonPath, pythonPath, 'Resolved Workspace Python Path is incorrect'); - }); - - test('Updating Workspace Folder Python Path using the factor service should work', async () => { - const workspaceUri = workspace3Uri; - const factory = new PythonPathUpdaterServiceFactory(); - const workspaceUpdater = factory.getWorkspaceFolderPythonPathConfigurationService(workspace.getWorkspaceFolder(workspaceUri)!.uri); - const pythonPath = `xWorkspacePythonPathFromFactory${new Date().getMilliseconds()}`; - await workspaceUpdater.updatePythonPath(pythonPath); - const folderValue = workspace.getConfiguration('python', workspace3Uri).inspect('pythonPath')!.workspaceFolderValue!; - assert.equal(folderValue, pythonPath, 'Workspace Python Path not updated'); - }); - - test('Updating Workspace Python Path using the PythonPathUpdaterService should work', async () => { - const workspaceUri = workspace3Uri; - const interpreterVersionService = ioc.serviceContainer.get(IInterpreterVersionService); - const updaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), interpreterVersionService); - const pythonPath = `xWorkspacePythonPathFromUpdater${new Date().getMilliseconds()}`; - await updaterService.updatePythonPath(pythonPath, ConfigurationTarget.WorkspaceFolder, 'ui', workspace.getWorkspaceFolder(workspaceUri)!.uri); - const folderValue = workspace.getConfiguration('python', workspace3Uri).inspect('pythonPath')!.workspaceFolderValue!; - assert.equal(folderValue, pythonPath, 'Workspace Python Path not updated'); - }); - - // tslint:disable-next-line:no-invalid-template-strings - test('Python Paths containing ${workspaceRoot} should be resolved as ${workspaceFolder}', async () => { - const workspaceUri = workspace.getWorkspaceFolder(workspace3Uri)!.uri; - const pythonInterpreter = `xWorkspacePythonPath${new Date().getMilliseconds()}`; - // tslint:disable-next-line:no-invalid-template-strings - const pythonPath = path.join('${workspaceRoot}', 'x', 'y', 'z', pythonInterpreter); - const workspaceUpdater = new WorkspacePythonPathUpdaterService(workspaceUri); - await workspaceUpdater.updatePythonPath(pythonPath); - // tslint:disable-next-line:no-any - const workspaceValue = workspace.getConfiguration('python', null as any as Uri).inspect('pythonPath')!.workspaceValue!; - const resolvedPythonPath = path.join(workspaceUri.fsPath, 'x', 'y', 'z', pythonInterpreter); - // tslint:disable-next-line:no-invalid-template-strings - assert.equal(workspaceValue, pythonPath, 'Workspace Python Path not updated'); - PythonSettings.dispose(); - assert.equal(PythonSettings.getInstance(workspace3Uri).pythonPath, resolvedPythonPath, 'Resolved Workspace Python Path is incorrect'); - }); - - // tslint:disable-next-line:no-invalid-template-strings - test('Python Path should be relative to workspace when using ${workspaceFolder}', async () => { - const workspaceUri = workspace.getWorkspaceFolder(workspace3Uri)!.uri; - const pythonInterpreter = `xWorkspacePythonPath${new Date().getMilliseconds()}`; - const pythonPath = path.join(workspaceUri.fsPath, 'x', 'y', 'z', pythonInterpreter); - const workspaceUpdater = new WorkspacePythonPathUpdaterService(workspaceUri); - await workspaceUpdater.updatePythonPath(pythonPath); - // tslint:disable-next-line:no-any - const workspaceValue = workspace.getConfiguration('python', null as any as Uri).inspect('pythonPath')!.workspaceValue!; - // tslint:disable-next-line:no-invalid-template-strings - assert.equal(workspaceValue, path.join('${workspaceFolder}', 'x', 'y', 'z', pythonInterpreter), 'Workspace Python Path not updated'); - PythonSettings.dispose(); - assert.equal(PythonSettings.getInstance(workspace3Uri).pythonPath, pythonPath, 'Resolved Workspace Python Path is incorrect'); - }); -}); diff --git a/src/test/interpreters/pythonPathUpdater.test.ts b/src/test/interpreters/pythonPathUpdater.test.ts index 849ce5fe71f2..e4865a9a9b68 100644 --- a/src/test/interpreters/pythonPathUpdater.test.ts +++ b/src/test/interpreters/pythonPathUpdater.test.ts @@ -1,111 +1,131 @@ -import * as assert from 'assert'; import * as path from 'path'; -import { ConfigurationTarget, Uri, workspace } from 'vscode'; -import { PythonSettings } from '../../client/common/configSettings'; -import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, Uri, WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../../client/common/application/types'; import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; -import { WorkspacePythonPathUpdaterService } from '../../client/interpreter/configuration/services/workspaceUpdaterService'; -import { IInterpreterVersionService } from '../../client/interpreter/contracts'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; -import { UnitTestIocContainer } from '../unittests/serviceRegistry'; +import { IPythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/types'; +import { IServiceContainer } from '../../client/ioc/types'; -const workspaceRoot = path.join(__dirname, '..', '..', '..', 'src', 'test'); +// tslint:disable:no-invalid-template-strings max-func-body-length -// tslint:disable-next-line:max-func-body-length suite('Python Path Settings Updater', () => { - let ioc: UnitTestIocContainer; - suiteSetup(initialize); - setup(async () => { - await initializeTest(); - initializeDI(); - }); - suiteTeardown(async () => { - await closeActiveWindows(); - await initializeTest(); - }); - teardown(async () => { - await closeActiveWindows(); - await initializeTest(); - ioc.dispose(); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerProcessTypes(); - ioc.registerVariableTypes(); - ioc.registerInterpreterTypes(); + let serviceContainer: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + let updaterServiceFactory: IPythonPathUpdaterServiceFactory; + function setupMocks() { + serviceContainer = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); + updaterServiceFactory = new PythonPathUpdaterServiceFactory(serviceContainer.object); + } + function setupConfigProvider(resource?: Uri): TypeMoq.IMock { + const workspaceConfig = TypeMoq.Mock.ofType(); + workspaceService.setup(w => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isValue(resource))).returns(() => workspaceConfig.object); + return workspaceConfig; } + suite('Global', () => { + setup(setupMocks); + test('Python Path should not be updated when current pythonPath is the same', async () => { + const updater = updaterServiceFactory.getGlobalPythonPathConfigurationService(); + const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; + const workspaceConfig = setupConfigProvider(); + workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => { + // tslint:disable-next-line:no-any + return { globalValue: pythonPath } as any; + }); - // Create Github issue VS Code bug (global changes not reflected immediately) + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify(w => w.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); + }); + test('Python Path should be updated when current pythonPath is different', async () => { + const updater = updaterServiceFactory.getGlobalPythonPathConfigurationService(); + const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; + const workspaceConfig = setupConfigProvider(); + workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); - // test('Updating Global Python Path should work', async () => { - // const globalUpdater = new GlobalPythonPathUpdaterService(); - // const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; - // await globalUpdater.updatePythonPath(pythonPath); - // const globalPythonValue = workspace.getConfiguration('python').inspect('pythonPath').globalValue; - // assert.equal(globalPythonValue, pythonPath, 'Global Python Path not updated'); - // }); + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify(w => w.update(TypeMoq.It.isValue('pythonPath'), TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(true)), TypeMoq.Times.once()); + }); + }); - // test('Updating Global Python Path using the factory service should work', async () => { - // const globalUpdater = new PythonPathUpdaterServiceFactory().getGlobalPythonPathConfigurationService(); - // const pythonPath = `xGlobalPythonPathFromFactory${new Date().getMilliseconds()}`; - // await globalUpdater.updatePythonPath(pythonPath); - // const globalPythonValue = workspace.getConfiguration('python').inspect('pythonPath').globalValue; - // assert.equal(globalPythonValue, pythonPath, 'Global Python Path not updated'); - // }); + suite('WorkspaceFolder', () => { + setup(setupMocks); + test('Python Path should not be updated when current pythonPath is the same', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => { + // tslint:disable-next-line:no-any + return { workspaceFolderValue: pythonPath } as any; + }); - // test('Updating Global Python Path using the PythonPathUpdaterService should work', async () => { - // const updaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory()); - // const pythonPath = `xGlobalPythonPathFromUpdater${new Date().getMilliseconds()}`; - // await updaterService.updatePythonPath(pythonPath, ConfigurationTarget.Global); - // const globalPythonValue = workspace.getConfiguration('python').inspect('pythonPath').globalValue; - // assert.equal(globalPythonValue, pythonPath, 'Global Python Path not updated'); - // }); + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify(w => w.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); + }); + test('Python Path should be updated when current pythonPath is different', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); - test('Updating Workspace Python Path should work', async () => { - const workspaceUri = Uri.file(workspaceRoot); - const workspaceUpdater = new WorkspacePythonPathUpdaterService(workspace.getWorkspaceFolder(workspaceUri)!.uri); - const pythonPath = `xWorkspacePythonPath${new Date().getMilliseconds()}`; - await workspaceUpdater.updatePythonPath(pythonPath); - const workspaceValue = workspace.getConfiguration('python').inspect('pythonPath')!.workspaceValue!; - assert.equal(workspaceValue, pythonPath, 'Workspace Python Path not updated'); - }); + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify(w => w.update(TypeMoq.It.isValue('pythonPath'), TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder)), TypeMoq.Times.once()); + }); + test('Python Path should be updated with ${workspaceFolder} for relative paths', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); + const pythonPath = Uri.file(path.join(workspaceFolderPath, 'env', 'bin', 'python')).fsPath; + const expectedPythonPath = path.join('${workspaceFolder}', 'env', 'bin', 'python'); + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); - test('Updating Workspace Python Path using the factor service should work', async () => { - const workspaceUri = Uri.file(workspaceRoot); - const factory = new PythonPathUpdaterServiceFactory(); - const workspaceUpdater = factory.getWorkspacePythonPathConfigurationService(workspace.getWorkspaceFolder(workspaceUri)!.uri); - const pythonPath = `xWorkspacePythonPathFromFactory${new Date().getMilliseconds()}`; - await workspaceUpdater.updatePythonPath(pythonPath); - // tslint:disable-next-line:no-any - const workspaceValue = workspace.getConfiguration('python', null as any as Uri).inspect('pythonPath')!.workspaceValue!; - assert.equal(workspaceValue, pythonPath, 'Workspace Python Path not updated'); + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify(w => w.update(TypeMoq.It.isValue('pythonPath'), TypeMoq.It.isValue(expectedPythonPath), TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder)), TypeMoq.Times.once()); + }); }); + suite('Workspace (multiroot scenario)', () => { + setup(setupMocks); + test('Python Path should not be updated when current pythonPath is the same', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => { + // tslint:disable-next-line:no-any + return { workspaceValue: pythonPath } as any; + }); - test('Updating Workspace Python Path using the PythonPathUpdaterService should work', async () => { - const workspaceUri = Uri.file(workspaceRoot); - const interpreterVersionService = ioc.serviceContainer.get(IInterpreterVersionService); - const updaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), interpreterVersionService); - const pythonPath = `xWorkspacePythonPathFromUpdater${new Date().getMilliseconds()}`; - await updaterService.updatePythonPath(pythonPath, ConfigurationTarget.Workspace, 'ui', workspace.getWorkspaceFolder(workspaceUri)!.uri); - // tslint:disable-next-line:no-any - const workspaceValue = workspace.getConfiguration('python', null as any as Uri).inspect('pythonPath')!.workspaceValue!; - assert.equal(workspaceValue, pythonPath, 'Workspace Python Path not updated'); - }); + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify(w => w.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); + }); + test('Python Path should be updated when current pythonPath is different', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); - test('Python Path should be relative to workspaceFolder', async () => { - const workspaceUri = workspace.getWorkspaceFolder(Uri.file(workspaceRoot))!.uri; - const pythonInterpreter = `xWorkspacePythonPath${new Date().getMilliseconds()}`; - const pythonPath = path.join(workspaceUri.fsPath, 'x', 'y', 'z', pythonInterpreter); - const workspaceUpdater = new WorkspacePythonPathUpdaterService(workspaceUri); - await workspaceUpdater.updatePythonPath(pythonPath); - // tslint:disable-next-line:no-any - const workspaceValue = workspace.getConfiguration('python', null as any as Uri).inspect('pythonPath')!.workspaceValue!; - // tslint:disable-next-line:no-invalid-template-strings - assert.equal(workspaceValue, path.join('${workspaceFolder}', 'x', 'y', 'z', pythonInterpreter), 'Workspace Python Path not updated'); - const resolvedPath = PythonSettings.getInstance(Uri.file(workspaceRoot)).pythonPath; - assert.equal(resolvedPath, pythonPath, 'Resolved Workspace Python Path not updated'); - }); + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify(w => w.update(TypeMoq.It.isValue('pythonPath'), TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(false)), TypeMoq.Times.once()); + }); + test('Python Path should be updated with ${workspaceFolder} for relative paths', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); + const pythonPath = Uri.file(path.join(workspaceFolderPath, 'env', 'bin', 'python')).fsPath; + const expectedPythonPath = path.join('${workspaceFolder}', 'env', 'bin', 'python'); + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify(w => w.update(TypeMoq.It.isValue('pythonPath'), TypeMoq.It.isValue(expectedPythonPath), TypeMoq.It.isValue(false)), TypeMoq.Times.once()); + }); + }); }); diff --git a/src/test/linters/lint.multiroot.test.ts b/src/test/linters/lint.multiroot.test.ts index 038c2efa82c4..6ec8508c3371 100644 --- a/src/test/linters/lint.multiroot.test.ts +++ b/src/test/linters/lint.multiroot.test.ts @@ -36,7 +36,7 @@ suite('Multiroot Linting', () => { function initializeDI() { ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); + ioc.registerCommonTypes(false); ioc.registerProcessTypes(); ioc.registerLinterTypes(); ioc.registerVariableTypes(); diff --git a/src/test/linters/lint.test.ts b/src/test/linters/lint.test.ts index 571cc1cd5a7a..906bdb2eef96 100644 --- a/src/test/linters/lint.test.ts +++ b/src/test/linters/lint.test.ts @@ -114,7 +114,7 @@ suite('Linting', () => { function initializeDI() { ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); + ioc.registerCommonTypes(false); ioc.registerProcessTypes(); ioc.registerLinterTypes(); ioc.registerVariableTypes(); diff --git a/src/test/providers/shebangCodeLenseProvider.test.ts b/src/test/providers/shebangCodeLenseProvider.test.ts index 5175a82b85a0..bc89c1761d5e 100644 --- a/src/test/providers/shebangCodeLenseProvider.test.ts +++ b/src/test/providers/shebangCodeLenseProvider.test.ts @@ -4,7 +4,6 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { CancellationTokenSource } from 'vscode'; import { IS_WINDOWS, PythonSettings } from '../../client/common/configSettings'; -import { IProcessService } from '../../client/common/process/types'; import { ShebangCodeLensProvider } from '../../client/interpreter/display/shebangCodeLensProvider'; import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers'; import { closeActiveWindows, initialize, initializeTest } from '../initialize'; @@ -103,8 +102,7 @@ suite('Shebang detection', () => { } async function setupCodeLens(document: vscode.TextDocument) { - const processService = ioc.serviceContainer.get(IProcessService); - const codeLensProvider = new ShebangCodeLensProvider(processService); + const codeLensProvider = new ShebangCodeLensProvider(ioc.serviceContainer); return await codeLensProvider.provideCodeLenses(document, new CancellationTokenSource().token); } }); diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts index 11a4c39a622c..422501a6cd07 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -6,8 +6,11 @@ import { Disposable, Memento, OutputChannel, Uri } from 'vscode'; import { STANDARD_OUTPUT_CHANNEL } from '../client/common/constants'; import { Logger } from '../client/common/logger'; import { IS_64_BIT, IS_WINDOWS } from '../client/common/platform/constants'; +import { FileSystem } from '../client/common/platform/fileSystem'; import { PathUtils } from '../client/common/platform/pathUtils'; +import { PlatformService } from '../client/common/platform/platformService'; import { registerTypes as platformRegisterTypes } from '../client/common/platform/serviceRegistry'; +import { IFileSystem, IPlatformService } from '../client/common/platform/types'; import { BufferDecoder } from '../client/common/process/decoder'; import { ProcessService } from '../client/common/process/proc'; import { PythonExecutionFactory } from '../client/common/process/pythonExecutionFactory'; @@ -62,8 +65,15 @@ export class IocContainer { this.disposables.forEach(disposable => disposable.dispose()); } - public registerCommonTypes() { + public registerCommonTypes(registerFileSystem: boolean = true) { commonRegisterTypes(this.serviceManager); + if (registerFileSystem) { + this.registerFileSystemTypes(); + } + } + public registerFileSystemTypes() { + this.serviceManager.addSingleton(IPlatformService, PlatformService); + this.serviceManager.addSingleton(IFileSystem, FileSystem); } public registerProcessTypes() { processRegisterTypes(this.serviceManager); diff --git a/yarn.lock b/yarn.lock index 0f981cb77091..986e27c2d9c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -70,6 +70,12 @@ version "4.14.91" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.91.tgz#794611b28056d16b5436059c6d800b39d573cd3a" +"@types/md5@^2.1.32": + version "2.1.32" + resolved "https://registry.yarnpkg.com/@types/md5/-/md5-2.1.32.tgz#93e23437fcd17a7b9ca98d02aa6002e835842fe8" + dependencies: + "@types/node" "*" + "@types/minimatch@*": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.2.tgz#09c06877e478a5d5f32ce5017c2eb2b33006f6f5" @@ -100,6 +106,10 @@ version "2.3.7" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-2.3.7.tgz#e92c2fed3297eae078d78d1da032b26788b4af86" +"@types/untildify@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/untildify/-/untildify-3.0.0.tgz#cd3e6624e46ccf292d3823fb48fa90dda0deaec0" + "@types/uuid@^3.3.27": version "3.4.3" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.3.tgz#121ace265f5569ce40f4f6d0ff78a338c732a754" @@ -580,6 +590,10 @@ chalk@^2.1.0: escape-string-regexp "^1.0.5" supports-color "^4.0.0" +charenc@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + check-error@^1.0.1, check-error@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" @@ -755,6 +769,10 @@ core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" +crypt@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + cryptiles@2.x.x: version "2.0.5" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" @@ -1972,7 +1990,7 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" -is-buffer@^1.1.5: +is-buffer@^1.1.5, is-buffer@~1.1.1: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -2678,6 +2696,14 @@ md5.js@1.3.4: hash-base "^3.0.0" inherits "^2.0.1" +md5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" + dependencies: + charenc "~0.0.1" + crypt "~0.0.1" + is-buffer "~1.1.1" + meow@^3.3.0: version "3.7.0" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"