From a4374fe504a6746ffb95793d0db3bab8acd0b61b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 10 May 2018 06:57:00 -0700 Subject: [PATCH] Create a new API to retrieve interpreter details with the ability to cache the details (#1567) Fixes #1569 --- news/3 Code Health/1569.md | 1 + pythonFiles/interpreterInfo.py | 13 ++++ .../activation/interpreterDataService.ts | 2 +- src/client/common/installer/channelManager.ts | 6 +- src/client/common/installer/pipInstaller.ts | 2 +- .../common/installer/productInstaller.ts | 2 +- src/client/common/persistentState.ts | 29 +++++--- src/client/common/platform/fileSystem.ts | 13 ++++ src/client/common/platform/types.ts | 1 + .../common/process/pythonExecutionFactory.ts | 12 ++-- src/client/common/process/pythonProcess.ts | 41 +++++++---- .../common/process/pythonToolService.ts | 4 +- src/client/common/process/types.ts | 21 +++++- src/client/common/types.ts | 4 +- .../configurationProviderUtils.ts | 2 +- .../configuration/pythonPathUpdaterService.ts | 19 +++--- src/client/interpreter/contracts.ts | 21 +++--- src/client/interpreter/display/index.ts | 16 ++--- src/client/interpreter/helpers.ts | 38 ++++++++++- src/client/interpreter/interpreterService.ts | 33 ++++++--- src/client/interpreter/locators/index.ts | 5 ++ .../locators/services/KnownPathsService.ts | 28 ++++---- .../services/baseVirtualEnvService.ts | 30 ++++---- .../locators/services/condaEnvFileService.ts | 14 ++-- .../locators/services/condaEnvService.ts | 21 +++--- .../locators/services/currentPathService.ts | 25 ++++--- .../locators/services/pipEnvService.ts | 49 +++++++++---- .../services/windowsRegistryService.ts | 34 ++++++---- src/client/interpreter/serviceRegistry.ts | 2 + src/client/interpreter/virtualEnvs/index.ts | 68 +++++++++++++++++-- src/client/interpreter/virtualEnvs/types.ts | 4 ++ .../linters/errorHandlers/notInstalled.ts | 4 +- src/client/providers/importSortProvider.ts | 2 +- src/client/providers/jediProxy.ts | 4 +- src/client/refactor/proxy.ts | 2 +- src/client/unittests/common/runner.ts | 7 +- src/test/common/moduleInstaller.test.ts | 24 +++++-- .../pythonProc.simple.multiroot.test.ts | 10 +-- .../configuration/interpreterSelector.test.ts | 17 ++++- src/test/format/extension.format.test.ts | 4 +- .../install/channelManager.channels.test.ts | 15 ++++ .../install/channelManager.messages.test.ts | 16 ++++- src/test/install/pythonInstallation.test.ts | 20 +++++- .../interpreters/condaEnvFileService.test.ts | 16 ++--- src/test/interpreters/condaEnvService.test.ts | 29 ++++---- src/test/interpreters/condaService.test.ts | 36 +++++++--- .../interpreters/currentPathService.test.ts | 16 +++-- src/test/interpreters/display.test.ts | 35 +++++++--- src/test/interpreters/helper.test.ts | 2 +- .../interpreters/interpreterService.test.ts | 19 +++++- src/test/interpreters/pipEnvService.test.ts | 26 +++---- .../interpreters/virtualEnvManager.test.ts | 12 ++++ .../windowsRegistryService.test.ts | 9 ++- src/test/unittests/serviceRegistry.ts | 2 +- 54 files changed, 631 insertions(+), 256 deletions(-) create mode 100644 news/3 Code Health/1569.md create mode 100644 pythonFiles/interpreterInfo.py diff --git a/news/3 Code Health/1569.md b/news/3 Code Health/1569.md new file mode 100644 index 000000000000..46eca178ee8a --- /dev/null +++ b/news/3 Code Health/1569.md @@ -0,0 +1 @@ +Create a new API to retrieve interpreter details with the ability to cache the details. diff --git a/pythonFiles/interpreterInfo.py b/pythonFiles/interpreterInfo.py new file mode 100644 index 000000000000..4822594bd046 --- /dev/null +++ b/pythonFiles/interpreterInfo.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import sys + +obj = {} +obj["versionInfo"] = sys.version_info[:4] +obj["sysPrefix"] = sys.prefix +obj["version"] = sys.version +obj["is64Bit"] = sys.maxsize > 2**32 + +print(json.dumps(obj)) diff --git a/src/client/activation/interpreterDataService.ts b/src/client/activation/interpreterDataService.ts index 45c7e42006c4..f923758b36bf 100644 --- a/src/client/activation/interpreterDataService.ts +++ b/src/client/activation/interpreterDataService.ts @@ -33,7 +33,7 @@ export class InterpreterDataService { public async getInterpreterData(resource?: Uri): Promise { const executionFactory = this.serviceContainer.get(IPythonExecutionFactory); - const execService = await executionFactory.create(resource); + const execService = await executionFactory.create({ resource }); const interpreterPath = await execService.getExecutablePath(); if (interpreterPath.length === 0) { diff --git a/src/client/common/installer/channelManager.ts b/src/client/common/installer/channelManager.ts index 3e3747f3d412..7c5780d9152e 100644 --- a/src/client/common/installer/channelManager.ts +++ b/src/client/common/installer/channelManager.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { QuickPickItem, Uri } from 'vscode'; +import { Uri } from 'vscode'; import { IInterpreterService, InterpreterType } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { IApplicationShell } from '../application/types'; @@ -34,9 +34,9 @@ export class InstallationChannelManager implements IInstallationChannelManager { label: `Install using ${installer.displayName}`, description: '', installer - } as QuickPickItem & { installer: IModuleInstaller }; + }; }); - const selection = await appShell.showQuickPick(options, { matchOnDescription: true, matchOnDetail: true, placeHolder }); + const selection = await appShell.showQuickPick(options, { matchOnDescription: true, matchOnDetail: true, placeHolder }); return selection ? selection.installer : undefined; } diff --git a/src/client/common/installer/pipInstaller.ts b/src/client/common/installer/pipInstaller.ts index bfc7ff4deadd..90a471b382b1 100644 --- a/src/client/common/installer/pipInstaller.ts +++ b/src/client/common/installer/pipInstaller.ts @@ -38,7 +38,7 @@ export class PipInstaller extends ModuleInstaller implements IModuleInstaller { } private isPipAvailable(resource?: Uri): Promise { const pythonExecutionFactory = this.serviceContainer.get(IPythonExecutionFactory); - return pythonExecutionFactory.create(resource) + return pythonExecutionFactory.create({ resource }) .then(proc => proc.isModuleInstalled('pip')) .catch(() => false); } diff --git a/src/client/common/installer/productInstaller.ts b/src/client/common/installer/productInstaller.ts index d214653c6f43..6c56fdc4da7d 100644 --- a/src/client/common/installer/productInstaller.ts +++ b/src/client/common/installer/productInstaller.ts @@ -74,7 +74,7 @@ abstract class BaseInstaller { const isModule = typeof moduleName === 'string' && moduleName.length > 0 && path.basename(executableName) === executableName; if (isModule) { - const pythonProcess = await this.serviceContainer.get(IPythonExecutionFactory).create(resource); + const pythonProcess = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource }); return pythonProcess.isModuleInstalled(executableName); } else { const process = await this.serviceContainer.get(IProcessServiceFactory).create(resource); diff --git a/src/client/common/persistentState.ts b/src/client/common/persistentState.ts index a882db7cbc1f..8042c7f52d2a 100644 --- a/src/client/common/persistentState.ts +++ b/src/client/common/persistentState.ts @@ -8,25 +8,38 @@ import { Memento } from 'vscode'; import { GLOBAL_MEMENTO, IMemento, IPersistentState, IPersistentStateFactory, WORKSPACE_MEMENTO } from './types'; class PersistentState implements IPersistentState{ - constructor(private storage: Memento, private key: string, private defaultValue: T) { } + constructor(private storage: Memento, private key: string, private defaultValue?: T, private expiryDurationMs?: number) { } public get value(): T { - return this.storage.get(this.key, this.defaultValue); + if (this.expiryDurationMs) { + const cachedData = this.storage.get<{ data?: T; expiry?: number }>(this.key, { data: this.defaultValue! }); + if (!cachedData || !cachedData.expiry || cachedData.expiry < Date.now()) { + return this.defaultValue!; + } else { + return cachedData.data!; + } + } else { + return this.storage.get(this.key, this.defaultValue!); + } } public async updateValue(newValue: T): Promise { - await this.storage.update(this.key, newValue); + if (this.expiryDurationMs) { + await this.storage.update(this.key, { data: newValue, expiry: Date.now() + this.expiryDurationMs }); + } else { + await this.storage.update(this.key, newValue); + } } } @injectable() export class PersistentStateFactory implements IPersistentStateFactory { - constructor( @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, + constructor(@inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, @inject(IMemento) @named(WORKSPACE_MEMENTO) private workspaceState: Memento) { } - public createGlobalPersistentState(key: string, defaultValue: T): IPersistentState { - return new PersistentState(this.globalState, key, defaultValue); + public createGlobalPersistentState(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState { + return new PersistentState(this.globalState, key, defaultValue, expiryDurationMs); } - public createWorkspacePersistentState(key: string, defaultValue: T): IPersistentState { - return new PersistentState(this.workspaceState, key, defaultValue); + public createWorkspacePersistentState(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState { + return new PersistentState(this.workspaceState, key, defaultValue, expiryDurationMs); } } diff --git a/src/client/common/platform/fileSystem.ts b/src/client/common/platform/fileSystem.ts index ecc9c564f446..fd409584e00d 100644 --- a/src/client/common/platform/fileSystem.ts +++ b/src/client/common/platform/fileSystem.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. 'use strict'; +import { createHash } from 'crypto'; import * as fs from 'fs-extra'; import { inject, injectable } from 'inversify'; import * as path from 'path'; @@ -117,4 +118,16 @@ export class FileSystem implements IFileSystem { fs.unlink(filename, err => err ? deferred.reject(err) : deferred.resolve()); return deferred.promise; } + public getFileHash(filePath: string): Promise { + return new Promise(resolve => { + fs.lstat(filePath, (err, stats) => { + if (err) { + resolve(); + } else { + const actual = createHash('sha512').update(`${stats.ctimeMs}-${stats.mtimeMs}`).digest('hex'); + resolve(actual); + } + }); + }); + } } diff --git a/src/client/common/platform/types.ts b/src/client/common/platform/types.ts index ce3836eb59a2..d3a7fe118505 100644 --- a/src/client/common/platform/types.ts +++ b/src/client/common/platform/types.ts @@ -46,4 +46,5 @@ export interface IFileSystem { getRealPath(path: string): Promise; copyFile(src: string, dest: string): Promise; deleteFile(filename: string): Promise; + getFileHash(filePath: string): Promise; } diff --git a/src/client/common/process/pythonExecutionFactory.ts b/src/client/common/process/pythonExecutionFactory.ts index ceafb3aa827b..3e9551905cec 100644 --- a/src/client/common/process/pythonExecutionFactory.ts +++ b/src/client/common/process/pythonExecutionFactory.ts @@ -4,17 +4,21 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; import { IServiceContainer } from '../../ioc/types'; +import { IConfigurationService } from '../types'; import { PythonExecutionService } from './pythonProcess'; -import { IProcessServiceFactory, IPythonExecutionFactory, IPythonExecutionService } from './types'; +import { ExecutionFactoryCreationOptions, IProcessServiceFactory, IPythonExecutionFactory, IPythonExecutionService } from './types'; @injectable() export class PythonExecutionFactory implements IPythonExecutionFactory { + private readonly configService: IConfigurationService; private processServiceFactory: IProcessServiceFactory; constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); + this.configService = serviceContainer.get(IConfigurationService); } - public async create(resource?: Uri): Promise { - const processService = await this.processServiceFactory.create(resource); - return new PythonExecutionService(this.serviceContainer, processService, resource); + public async create(options: ExecutionFactoryCreationOptions): Promise { + const pythonPath = options.pythonPath ? options.pythonPath : this.configService.getSettings(options.resource).pythonPath; + const processService = await this.processServiceFactory.create(options.resource); + return new PythonExecutionService(this.serviceContainer, processService, pythonPath); } } diff --git a/src/client/common/process/pythonProcess.ts b/src/client/common/process/pythonProcess.ts index 8a7894dc4f54..625a02f34e5b 100644 --- a/src/client/common/process/pythonProcess.ts +++ b/src/client/common/process/pythonProcess.ts @@ -2,28 +2,46 @@ // Licensed under the MIT License. import { injectable } from 'inversify'; +import * as path from 'path'; import { Uri } from 'vscode'; -import { IInterpreterVersionService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; +import { EXTENSION_ROOT_DIR } from '../constants'; import { ErrorUtils } from '../errors/errorUtils'; import { ModuleNotInstalledError } from '../errors/moduleNotInstalledError'; -import { IFileSystem } from '../platform/types'; -import { IConfigurationService } from '../types'; -import { ExecutionResult, IProcessService, IPythonExecutionService, ObservableExecutionResult, SpawnOptions } from './types'; +import { Architecture, IFileSystem } from '../platform/types'; +import { EnvironmentVariables } from '../variables/types'; +import { ExecutionResult, InterpreterInfomation, IProcessService, IPythonExecutionService, ObservableExecutionResult, PythonVersionInfo, SpawnOptions } from './types'; @injectable() export class PythonExecutionService implements IPythonExecutionService { - private readonly configService: IConfigurationService; private readonly fileSystem: IFileSystem; - constructor(private serviceContainer: IServiceContainer, private readonly procService: IProcessService, private resource?: Uri) { - this.configService = serviceContainer.get(IConfigurationService); + constructor(private serviceContainer: IServiceContainer, private readonly procService: IProcessService, private readonly pythonPath: string) { this.fileSystem = serviceContainer.get(IFileSystem); } - public async getVersion(): Promise { - const versionService = this.serviceContainer.get(IInterpreterVersionService); - return versionService.getVersion(this.pythonPath, ''); + public async getInterpreterInformation(): Promise { + const file = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'interpreterInfo.py'); + try { + const [version, jsonValue] = await Promise.all([ + this.procService.exec(this.pythonPath, ['--version'], { mergeStdOutErr: true }) + .then(output => output.stdout.trim()), + this.procService.exec(this.pythonPath, [file], { mergeStdOutErr: true }) + .then(output => output.stdout.trim()) + ]); + + const json = JSON.parse(jsonValue) as { versionInfo: PythonVersionInfo; sysPrefix: string; sysVersion: string; is64Bit: boolean }; + return { + architecture: json.is64Bit ? Architecture.x64 : Architecture.x86, + path: this.pythonPath, + version, + sysVersion: json.sysVersion, + version_info: json.versionInfo, + sysPrefix: json.sysPrefix + }; + } catch (ex) { + console.error(`Failed to get interpreter information for '${this.pythonPath}'`, ex); + } } public async getExecutablePath(): Promise { // If we've passed the python file, then return the file. @@ -65,7 +83,4 @@ export class PythonExecutionService implements IPythonExecutionService { return result; } - private get pythonPath(): string { - return this.configService.getSettings(this.resource).pythonPath; - } } diff --git a/src/client/common/process/pythonToolService.ts b/src/client/common/process/pythonToolService.ts index 3369c35ac6b6..d4b2ccaaa8bb 100644 --- a/src/client/common/process/pythonToolService.ts +++ b/src/client/common/process/pythonToolService.ts @@ -15,7 +15,7 @@ export class PythonToolExecutionService implements IPythonToolExecutionService { throw new Error('Environment variables are not supported'); } if (executionInfo.moduleName && executionInfo.moduleName.length > 0) { - const pythonExecutionService = await this.serviceContainer.get(IPythonExecutionFactory).create(resource); + const pythonExecutionService = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource }); return pythonExecutionService.execModuleObservable(executionInfo.moduleName, executionInfo.args, options); } else { const processService = await this.serviceContainer.get(IProcessServiceFactory).create(resource); @@ -27,7 +27,7 @@ export class PythonToolExecutionService implements IPythonToolExecutionService { throw new Error('Environment variables are not supported'); } if (executionInfo.moduleName && executionInfo.moduleName.length > 0) { - const pythonExecutionService = await this.serviceContainer.get(IPythonExecutionFactory).create(resource); + const pythonExecutionService = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource }); return pythonExecutionService.execModule(executionInfo.moduleName!, executionInfo.args, options); } else { const processService = await this.serviceContainer.get(IProcessServiceFactory).create(resource); diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts index 22fb6965be55..96862c8ca26d 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -4,6 +4,7 @@ import { ChildProcess, SpawnOptions as ChildProcessSpawnOptions } from 'child_process'; import { Observable } from 'rxjs/Observable'; import { CancellationToken, Uri } from 'vscode'; +import { Architecture } from '../platform/types'; import { ExecutionInfo } from '../types'; import { EnvironmentVariables } from '../variables/types'; @@ -46,14 +47,28 @@ export interface IProcessServiceFactory { } export const IPythonExecutionFactory = Symbol('IPythonExecutionFactory'); - +export type ExecutionFactoryCreationOptions = { + resource?: Uri; + pythonPath?: string; +}; export interface IPythonExecutionFactory { - create(resource?: Uri): Promise; + create(options: ExecutionFactoryCreationOptions): Promise; } - +export type ReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final'; +// tslint:disable-next-line:interface-name +export type PythonVersionInfo = [number, number, number, ReleaseLevel]; +export type InterpreterInfomation = { + path: string; + version: string; + sysVersion: string; + architecture: Architecture; + version_info: PythonVersionInfo; + sysPrefix: string; +}; export const IPythonExecutionService = Symbol('IPythonExecutionService'); export interface IPythonExecutionService { + getInterpreterInformation(): Promise; getExecutablePath(): Promise; isModuleInstalled(moduleName: string): Promise; diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 11ff9b508d6b..2bec7bb18256 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -23,8 +23,8 @@ export interface IPersistentState { export const IPersistentStateFactory = Symbol('IPersistentStateFactory'); export interface IPersistentStateFactory { - createGlobalPersistentState(key: string, defaultValue: T): IPersistentState; - createWorkspacePersistentState(key: string, defaultValue: T): IPersistentState; + createGlobalPersistentState(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState; + createWorkspacePersistentState(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState; } export type ExecutionInfo = { diff --git a/src/client/debugger/configProviders/configurationProviderUtils.ts b/src/client/debugger/configProviders/configurationProviderUtils.ts index 6bc13f0bf53f..108426f9b979 100644 --- a/src/client/debugger/configProviders/configurationProviderUtils.ts +++ b/src/client/debugger/configProviders/configurationProviderUtils.ts @@ -24,7 +24,7 @@ export class ConfigurationProviderUtils implements IConfigurationProviderUtils { } public async getPyramidStartupScriptFilePath(resource?: Uri): Promise { try { - const executionService = await this.executionFactory.create(resource); + const executionService = await this.executionFactory.create({ resource }); const output = await executionService.exec(['-c', 'import pyramid;print(pyramid.__file__)'], { throwOnStdErr: true }); const pserveFilePath = path.join(path.dirname(output.stdout.trim()), 'scripts', PSERVE_SCRIPT_FILE_NAME); return await this.fs.fileExists(pserveFilePath) ? pserveFilePath : undefined; diff --git a/src/client/interpreter/configuration/pythonPathUpdaterService.ts b/src/client/interpreter/configuration/pythonPathUpdaterService.ts index 812fcd48b6de..e9579a565177 100644 --- a/src/client/interpreter/configuration/pythonPathUpdaterService.ts +++ b/src/client/interpreter/configuration/pythonPathUpdaterService.ts @@ -1,6 +1,7 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { ConfigurationTarget, Uri, window } from 'vscode'; +import { InterpreterInfomation, IPythonExecutionFactory } from '../../common/process/types'; import { StopWatch } from '../../common/stopWatch'; import { IServiceContainer } from '../../ioc/types'; import { sendTelemetryEvent } from '../../telemetry'; @@ -13,9 +14,11 @@ import { IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager } fr export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManager { private readonly pythonPathSettingsUpdaterFactory: IPythonPathUpdaterServiceFactory; private readonly interpreterVersionService: IInterpreterVersionService; + private readonly executionFactory: IPythonExecutionFactory; constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { this.pythonPathSettingsUpdaterFactory = serviceContainer.get(IPythonPathUpdaterServiceFactory); this.interpreterVersionService = serviceContainer.get(IInterpreterVersionService); + this.executionFactory = serviceContainer.get(IPythonExecutionFactory); } public async updatePythonPath(pythonPath: string, configTarget: ConfigurationTarget, trigger: 'ui' | 'shebang' | 'load', wkspace?: Uri): Promise { const stopWatch = new StopWatch(); @@ -39,17 +42,17 @@ export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManage failed, trigger }; if (!failed) { - const pyVersionPromise = this.interpreterVersionService.getVersion(pythonPath, '') - .then(pyVersion => pyVersion.length === 0 ? undefined : pyVersion); + const processService = await this.executionFactory.create({ pythonPath }); + const infoPromise = processService.getInterpreterInformation().catch(() => undefined); const pipVersionPromise = this.interpreterVersionService.getPipVersion(pythonPath) .then(value => value.length === 0 ? undefined : value) - .catch(() => undefined); - const versions = await Promise.all([pyVersionPromise, pipVersionPromise]); - if (versions[0]) { - telemtryProperties.version = versions[0] as string; + .catch(() => undefined); + const [info, pipVersion] = await Promise.all([infoPromise, pipVersionPromise]); + if (info) { + telemtryProperties.version = info.version; } - if (versions[1]) { - telemtryProperties.pipVersion = versions[1] as string; + if (pipVersion) { + telemtryProperties.pipVersion = pipVersion; } } sendTelemetryEvent(PYTHON_INTERPRETER, duration, telemtryProperties); diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index 9029a05dbb4e..1b3b793230bf 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -1,5 +1,5 @@ import { CodeLensProvider, ConfigurationTarget, Disposable, Event, TextDocument, Uri } from 'vscode'; -import { Architecture } from '../common/platform/types'; +import { InterpreterInfomation } from '../common/process/types'; export const INTERPRETER_LOCATOR_SERVICE = 'IInterpreterLocatorService'; export const WINDOWS_REGISTRY_SERVICE = 'WindowsRegistryService'; @@ -10,7 +10,6 @@ export const KNOWN_PATH_SERVICE = 'KnownPathsService'; export const GLOBAL_VIRTUAL_ENV_SERVICE = 'VirtualEnvService'; export const WORKSPACE_VIRTUAL_ENV_SERVICE = 'WorkspaceVirtualEnvService'; export const PIPENV_SERVICE = 'PipEnvService'; - export const IInterpreterVersionService = Symbol('IInterpreterVersionService'); export interface IInterpreterVersionService { getVersion(pythonPath: string, defaultValue: string): Promise; @@ -54,15 +53,14 @@ export interface ICondaService { export enum InterpreterType { Unknown = 1, Conda = 2, - VirtualEnv = 4 + VirtualEnv = 4, + PipEnv = 8, + Pyenv = 16, + Venv = 32 } - -export type PythonInterpreter = { - path: string; +export type PythonInterpreter = InterpreterInfomation & { companyDisplayName?: string; displayName?: string; - version?: string; - architecture?: Architecture; type: InterpreterType; envName?: string; envPath?: string; @@ -80,6 +78,7 @@ export interface IInterpreterService { getInterpreters(resource?: Uri): Promise; autoSetInterpreter(): Promise; getActiveInterpreter(resource?: Uri): Promise; + getInterpreterDetails(pythonPath: string): Promise>; refresh(): Promise; initialize(): void; } @@ -97,4 +96,10 @@ export interface IShebangCodeLensProvider extends CodeLensProvider { export const IInterpreterHelper = Symbol('IInterpreterHelper'); export interface IInterpreterHelper { getActiveWorkspaceUri(): WorkspacePythonPath | undefined; + getInterpreterInformation(pythonPath: string): Promise>; +} + +export const IPipEnvService = Symbol('IPipEnvService'); +export interface IPipEnvService { + isRelatedPipEnvironment(dir: string, pythonPath: string): Promise; } diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index e419691b31d2..14291ea76468 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -6,7 +6,7 @@ import { IApplicationShell, IWorkspaceService } from '../../common/application/t 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 { IInterpreterDisplay, IInterpreterHelper, IInterpreterService, PythonInterpreter } from '../contracts'; import { IVirtualEnvironmentManager } from '../virtualEnvs/types'; // tslint:disable-next-line:completed-docs @@ -15,7 +15,6 @@ 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; @@ -24,7 +23,6 @@ export class InterpreterDisplay implements IInterpreterDisplay { 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); @@ -63,17 +61,17 @@ export class InterpreterDisplay implements IInterpreterDisplay { this.statusBar.tooltip += toolTipSuffix; } } else { - const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; await Promise.all([ this.fileSystem.fileExists(pythonPath), - this.versionProvider.getVersion(pythonPath, defaultDisplayName), - this.getVirtualEnvironmentName(pythonPath).catch(() => '') + this.helper.getInterpreterInformation(pythonPath).catch>(() => undefined), + this.getVirtualEnvironmentName(pythonPath).catch(() => '') ]) - .then(([interpreterExists, displayName, virtualEnvName]) => { + .then(([interpreterExists, details, virtualEnvName]) => { + const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; const dislayNameSuffix = virtualEnvName.length > 0 ? ` (${virtualEnvName})` : ''; - this.statusBar.text = `${displayName}${dislayNameSuffix}`; + this.statusBar.text = `${details ? details.version : defaultDisplayName}${dislayNameSuffix}`; - if (!interpreterExists && displayName === defaultDisplayName && interpreters.length > 0) { + if (!interpreterExists && !details && interpreters.length > 0) { this.statusBar.color = 'yellow'; this.statusBar.text = '$(alert) Select Python Environment'; } diff --git a/src/client/interpreter/helpers.ts b/src/client/interpreter/helpers.ts index ba272607a147..00535634a590 100644 --- a/src/client/interpreter/helpers.ts +++ b/src/client/interpreter/helpers.ts @@ -1,8 +1,14 @@ import { inject, injectable } from 'inversify'; import { ConfigurationTarget } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../common/application/types'; +import { IFileSystem } from '../common/platform/types'; +import { IPythonExecutionFactory } from '../common/process/types'; +import { IPersistentStateFactory } from '../common/types'; import { IServiceContainer } from '../ioc/types'; -import { IInterpreterHelper, WorkspacePythonPath } from './contracts'; +import { IInterpreterHelper, PythonInterpreter, WorkspacePythonPath } from './contracts'; + +const EXPITY_DURATION = 24 * 60 * 60 * 1000; +type CachedPythonInterpreter = Partial & { fileHash: string }; export function getFirstNonEmptyLineFromMultilineString(stdout: string) { if (!stdout) { @@ -14,13 +20,17 @@ export function getFirstNonEmptyLineFromMultilineString(stdout: string) { @injectable() export class InterpreterHelper implements IInterpreterHelper { + private readonly fs: IFileSystem; + private readonly persistentFactory: IPersistentStateFactory; constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.persistentFactory = this.serviceContainer.get(IPersistentStateFactory); + this.fs = this.serviceContainer.get(IFileSystem); } 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) { + if (!workspaceService.hasWorkspaceFolders) { return; } if (workspaceService.workspaceFolders.length === 1) { @@ -33,4 +43,28 @@ export class InterpreterHelper implements IInterpreterHelper { } } } + public async getInterpreterInformation(pythonPath: string): Promise> { + const fileHash = await this.fs.getFileHash(pythonPath).catch(() => ''); + const store = this.persistentFactory.createGlobalPersistentState(pythonPath, undefined, EXPITY_DURATION); + if (store.value && store.value.fileHash === fileHash) { + return store.value; + } + const processService = await this.serviceContainer.get(IPythonExecutionFactory).create({ pythonPath }); + + try { + const info = await processService.getInterpreterInformation().catch(() => undefined); + if (!info) { + return; + } + const details = { + ...(info), + fileHash + }; + await store.updateValue(details); + return details; + } catch (ex) { + console.error(`Failed to get interpreter information for '${pythonPath}'`, ex); + return {}; + } + } } diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index 3dee82a4ceda..b0b69910241a 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -10,8 +10,8 @@ import { IServiceContainer } from '../ioc/types'; import { IPythonPathUpdaterServiceManager } from './configuration/types'; import { IInterpreterDisplay, IInterpreterHelper, IInterpreterLocatorService, - IInterpreterService, IInterpreterVersionService, INTERPRETER_LOCATOR_SERVICE, - InterpreterType, PIPENV_SERVICE, PythonInterpreter, WORKSPACE_VIRTUAL_ENV_SERVICE + IInterpreterService, INTERPRETER_LOCATOR_SERVICE, + PIPENV_SERVICE, PythonInterpreter, WORKSPACE_VIRTUAL_ENV_SERVICE } from './contracts'; import { IVirtualEnvironmentManager } from './virtualEnvs/types'; @@ -93,29 +93,40 @@ export class InterpreterService implements Disposable, IInterpreterService { public async getActiveInterpreter(resource?: Uri): Promise { const pythonExecutionFactory = this.serviceContainer.get(IPythonExecutionFactory); - const pythonExecutionService = await pythonExecutionFactory.create(resource); + const pythonExecutionService = await pythonExecutionFactory.create({ resource }); const fullyQualifiedPath = await pythonExecutionService.getExecutablePath().catch(() => undefined); // Python path is invalid or python isn't installed. if (!fullyQualifiedPath) { return; } + + return this.getInterpreterDetails(fullyQualifiedPath, resource); + } + public async getInterpreterDetails(pythonPath: string, resource?: Uri): Promise { const interpreters = await this.getInterpreters(resource); - const interpreter = interpreters.find(i => utils.arePathsSame(i.path, fullyQualifiedPath)); + const interpreter = interpreters.find(i => utils.arePathsSame(i.path, pythonPath)); if (interpreter) { return interpreter; } - const pythonExecutableName = path.basename(fullyQualifiedPath); - const versionInfo = await this.serviceContainer.get(IInterpreterVersionService).getVersion(fullyQualifiedPath, pythonExecutableName); + const interpreterHelper = this.serviceContainer.get(IInterpreterHelper); const virtualEnvManager = this.serviceContainer.get(IVirtualEnvironmentManager); - const virtualEnvName = await virtualEnvManager.getEnvironmentName(fullyQualifiedPath); + const [details, virtualEnvName, type] = await Promise.all([ + interpreterHelper.getInterpreterInformation(pythonPath), + virtualEnvManager.getEnvironmentName(pythonPath), + virtualEnvManager.getEnvironmentType(pythonPath) + ]); + if (details) { + return; + } const dislayNameSuffix = virtualEnvName.length > 0 ? ` (${virtualEnvName})` : ''; - const displayName = `${versionInfo}${dislayNameSuffix}`; + const displayName = `${details.version!}${dislayNameSuffix}`; return { + ...(details as PythonInterpreter), displayName, - path: fullyQualifiedPath, - type: virtualEnvName.length > 0 ? InterpreterType.VirtualEnv : InterpreterType.Unknown, - version: versionInfo + path: pythonPath, + envName: virtualEnvName, + type: type }; } private async shouldAutoSetInterpreter(): Promise { diff --git a/src/client/interpreter/locators/index.ts b/src/client/interpreter/locators/index.ts index f40667b70b44..e5e05ca49d8a 100644 --- a/src/client/interpreter/locators/index.ts +++ b/src/client/interpreter/locators/index.ts @@ -49,6 +49,8 @@ export class PythonInterpreterLocatorService implements IInterpreterLocatorServi // tslint:disable-next-line:underscore-consistent-invocation return _.flatten(listOfInterpreters) + .filter(item => !!item) + .map(item => item!) .map(fixInterpreterDisplayName) .map(item => { item.path = path.normalize(item.path); return item; }) .reduce((accumulator, current) => { @@ -70,6 +72,9 @@ export class PythonInterpreterLocatorService implements IInterpreterLocatorServi private getLocators(): IInterpreterLocatorService[] { const locators: IInterpreterLocatorService[] = []; // The order of the services is important. + // The order is important because the data sources at the bottom of the list do not contain all, + // the information about the interpreters (e.g. type, environment name, etc). + // This way, the items returned from the top of the list will win, when we combine the items returned. if (this.platform.isWindows) { locators.push(this.serviceContainer.get(IInterpreterLocatorService, WINDOWS_REGISTRY_SERVICE)); } diff --git a/src/client/interpreter/locators/services/KnownPathsService.ts b/src/client/interpreter/locators/services/KnownPathsService.ts index 1fb6b99f57fa..348bcf3d96a9 100644 --- a/src/client/interpreter/locators/services/KnownPathsService.ts +++ b/src/client/interpreter/locators/services/KnownPathsService.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { fsExistsAsync, IS_WINDOWS } from '../../../common/utils'; import { IServiceContainer } from '../../../ioc/types'; -import { IInterpreterVersionService, IKnownSearchPathsForInterpreters, InterpreterType, PythonInterpreter } from '../../contracts'; +import { IInterpreterHelper, IKnownSearchPathsForInterpreters, InterpreterType, PythonInterpreter } from '../../contracts'; import { lookForInterpretersInDirectory } from '../helpers'; import { CacheableLocatorService } from './cacheableLocatorService'; @@ -13,8 +13,8 @@ const untildify = require('untildify'); @injectable() export class KnownPathsService extends CacheableLocatorService { - public constructor( @inject(IKnownSearchPathsForInterpreters) private knownSearchPaths: string[], - @inject(IInterpreterVersionService) private versionProvider: IInterpreterVersionService, + public constructor(@inject(IKnownSearchPathsForInterpreters) private knownSearchPaths: string[], + @inject(IInterpreterHelper) private helper: IInterpreterHelper, @inject(IServiceContainer) serviceContainer: IServiceContainer) { super('KnownPathsService', serviceContainer); } @@ -29,17 +29,19 @@ export class KnownPathsService extends CacheableLocatorService { // tslint:disable-next-line:underscore-consistent-invocation .then(listOfInterpreters => _.flatten(listOfInterpreters)) .then(interpreters => interpreters.filter(item => item.length > 0)) - .then(interpreters => Promise.all(interpreters.map(interpreter => this.getInterpreterDetails(interpreter)))); + .then(interpreters => Promise.all(interpreters.map(interpreter => this.getInterpreterDetails(interpreter)))) + .then(interpreters => interpreters.filter(interpreter => !!interpreter).map(interpreter => interpreter!)); } - private getInterpreterDetails(interpreter: string) { - return this.versionProvider.getVersion(interpreter, path.basename(interpreter)) - .then(displayName => { - return { - displayName, - path: interpreter, - type: InterpreterType.Unknown - }; - }); + private async getInterpreterDetails(interpreter: string) { + const details = await this.helper.getInterpreterInformation(interpreter); + if (!details) { + return; + } + return { + ...(details as PythonInterpreter), + path: interpreter, + type: InterpreterType.Unknown + }; } private getInterpretersInDirectory(dir: string) { return fsExistsAsync(dir) diff --git a/src/client/interpreter/locators/services/baseVirtualEnvService.ts b/src/client/interpreter/locators/services/baseVirtualEnvService.ts index 4499a3a38acf..8c48d470d9bc 100644 --- a/src/client/interpreter/locators/services/baseVirtualEnvService.ts +++ b/src/client/interpreter/locators/services/baseVirtualEnvService.ts @@ -4,7 +4,7 @@ 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 { IInterpreterHelper, IVirtualEnvironmentsSearchPathProvider, PythonInterpreter } from '../../contracts'; import { IVirtualEnvironmentManager } from '../../virtualEnvs/types'; import { lookForInterpretersInDirectory } from '../helpers'; import { CacheableLocatorService } from './cacheableLocatorService'; @@ -12,7 +12,7 @@ import { CacheableLocatorService } from './cacheableLocatorService'; @injectable() export class BaseVirtualEnvService extends CacheableLocatorService { private readonly virtualEnvMgr: IVirtualEnvironmentManager; - private readonly versionProvider: IInterpreterVersionService; + private readonly helper: IInterpreterHelper; private readonly fileSystem: IFileSystem; public constructor(@unmanaged() private searchPathsProvider: IVirtualEnvironmentsSearchPathProvider, @unmanaged() serviceContainer: IServiceContainer, @@ -20,7 +20,7 @@ export class BaseVirtualEnvService extends CacheableLocatorService { @unmanaged() cachePerWorkspace: boolean = false) { super(name, serviceContainer, cachePerWorkspace); this.virtualEnvMgr = serviceContainer.get(IVirtualEnvironmentManager); - this.versionProvider = serviceContainer.get(IInterpreterVersionService); + this.helper = serviceContainer.get(IInterpreterHelper); this.fileSystem = serviceContainer.get(IFileSystem); } // tslint:disable-next-line:no-empty @@ -30,16 +30,17 @@ export class BaseVirtualEnvService extends CacheableLocatorService { } private async suggestionsFromKnownVenvs(resource?: Uri) { const searchPaths = this.searchPathsProvider.getSearchPaths(resource); - return Promise.all(searchPaths.map(dir => this.lookForInterpretersInVenvs(dir))) + return Promise.all(searchPaths.map(dir => this.lookForInterpretersInVenvs(dir, resource))) .then(listOfInterpreters => _.flatten(listOfInterpreters)); } - private async lookForInterpretersInVenvs(pathToCheck: string) { + private async lookForInterpretersInVenvs(pathToCheck: string, resource?: Uri) { return this.fileSystem.getSubDirectories(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)))) + .then(interpreters => interpreters.filter(interpreter => !!interpreter).map(interpreter => interpreter!)) .catch((err) => { console.error('Python Extension (lookForInterpretersInVenvs):', err); // Ignore exceptions. @@ -64,17 +65,22 @@ export class BaseVirtualEnvService extends CacheableLocatorService { return ''; })); } - private async getVirtualEnvDetails(interpreter: string): Promise { + private async getVirtualEnvDetails(interpreter: string): Promise { return Promise.all([ - this.versionProvider.getVersion(interpreter, path.basename(interpreter)), - this.virtualEnvMgr.getEnvironmentName(interpreter) + this.helper.getInterpreterInformation(interpreter), + this.virtualEnvMgr.getEnvironmentName(interpreter), + this.virtualEnvMgr.getEnvironmentType(interpreter) ]) - .then(([displayName, virtualEnvName]) => { + .then(([details, virtualEnvName, type]) => { + if (!details) { + return; + } const virtualEnvSuffix = virtualEnvName.length ? virtualEnvName : this.getVirtualEnvironmentRootDirectory(interpreter); return { - displayName: `${displayName} (${virtualEnvSuffix})`.trim(), - path: interpreter, - type: virtualEnvName.length > 0 ? InterpreterType.VirtualEnv : InterpreterType.Unknown + ...(details as PythonInterpreter), + displayName: `${details.version!} (${virtualEnvSuffix})`.trim(), + envName: virtualEnvName, + type: type }; }); } diff --git a/src/client/interpreter/locators/services/condaEnvFileService.ts b/src/client/interpreter/locators/services/condaEnvFileService.ts index 2f9e681abb3f..7984c2fb5495 100644 --- a/src/client/interpreter/locators/services/condaEnvFileService.ts +++ b/src/client/interpreter/locators/services/condaEnvFileService.ts @@ -1,12 +1,11 @@ import { inject, injectable } from 'inversify'; -import * as path from 'path'; import { Uri } from 'vscode'; import { IFileSystem } from '../../../common/platform/types'; import { ILogger } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; import { ICondaService, - IInterpreterVersionService, + IInterpreterHelper, InterpreterType, PythonInterpreter } from '../../contracts'; @@ -15,7 +14,7 @@ import { AnacondaCompanyName, AnacondaCompanyNames, AnacondaDisplayName } from ' @injectable() export class CondaEnvFileService extends CacheableLocatorService { - constructor(@inject(IInterpreterVersionService) private versionService: IInterpreterVersionService, + constructor(@inject(IInterpreterHelper) private helperService: IInterpreterHelper, @inject(ICondaService) private condaService: ICondaService, @inject(IFileSystem) private fileSystem: IFileSystem, @inject(IServiceContainer) serviceContainer: IServiceContainer, @@ -70,13 +69,16 @@ export class CondaEnvFileService extends CacheableLocatorService { return; } - const version = await this.versionService.getVersion(interpreter, path.basename(interpreter)); - const versionWithoutCompanyName = this.stripCompanyName(version); + const details = await this.helperService.getInterpreterInformation(interpreter); + if (!details) { + return; + } + const versionWithoutCompanyName = this.stripCompanyName(details.version!); return { displayName: `${AnacondaDisplayName} ${versionWithoutCompanyName}`, + ...(details as PythonInterpreter), path: interpreter, companyDisplayName: AnacondaCompanyName, - version: version, type: InterpreterType.Conda, envPath: environmentPath }; diff --git a/src/client/interpreter/locators/services/condaEnvService.ts b/src/client/interpreter/locators/services/condaEnvService.ts index 781f3b286f75..dfdde13f1001 100644 --- a/src/client/interpreter/locators/services/condaEnvService.ts +++ b/src/client/interpreter/locators/services/condaEnvService.ts @@ -6,7 +6,7 @@ import { Uri } from 'vscode'; import { IFileSystem } from '../../../common/platform/types'; import { ILogger } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; -import { CondaInfo, ICondaService, IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../contracts'; +import { CondaInfo, ICondaService, IInterpreterHelper, InterpreterType, PythonInterpreter } from '../../contracts'; import { CacheableLocatorService } from './cacheableLocatorService'; import { AnacondaCompanyName, AnacondaCompanyNames } from './conda'; import { CondaHelper } from './condaHelper'; @@ -15,7 +15,7 @@ import { CondaHelper } from './condaHelper'; export class CondaEnvService extends CacheableLocatorService { private readonly condaHelper = new CondaHelper(); constructor(@inject(ICondaService) private condaService: ICondaService, - @inject(IInterpreterVersionService) private versionService: IInterpreterVersionService, + @inject(IInterpreterHelper) private helper: IInterpreterHelper, @inject(ILogger) private logger: ILogger, @inject(IServiceContainer) serviceContainer: IServiceContainer, @inject(IFileSystem) private fileSystem: IFileSystem) { @@ -37,25 +37,24 @@ export class CondaEnvService extends CacheableLocatorService { .map(async envPath => { const pythonPath = this.condaService.getInterpreterPath(envPath); - const existsPromise = pythonPath ? this.fileSystem.fileExists(pythonPath) : Promise.resolve(false); - const versionPromise = this.versionService.getVersion(pythonPath, ''); - - const [exists, version] = await Promise.all([existsPromise, versionPromise]); - if (!exists) { + if (!(await this.fileSystem.fileExists(pythonPath))) { + return; + } + const details = await this.helper.getInterpreterInformation(pythonPath); + if (!details) { return; } - const versionWithoutCompanyName = this.stripCondaDisplayName(this.stripCompanyName(version), condaDisplayName); + const versionWithoutCompanyName = this.stripCondaDisplayName(this.stripCompanyName(details.version!), condaDisplayName); const displayName = `${condaDisplayName} ${versionWithoutCompanyName}`.trim(); - // tslint:disable-next-line:no-unnecessary-local-variable - const interpreter: PythonInterpreter = { + return { + ...(details as PythonInterpreter), path: pythonPath, displayName, companyDisplayName: AnacondaCompanyName, type: InterpreterType.Conda, envPath }; - return interpreter; }); return Promise.all(promises) diff --git a/src/client/interpreter/locators/services/currentPathService.ts b/src/client/interpreter/locators/services/currentPathService.ts index 5d8c6fc1b636..a4e81dbed34d 100644 --- a/src/client/interpreter/locators/services/currentPathService.ts +++ b/src/client/interpreter/locators/services/currentPathService.ts @@ -1,12 +1,11 @@ import { inject, injectable } from 'inversify'; import * as _ from 'lodash'; -import * as path from 'path'; import { Uri } from 'vscode'; import { IFileSystem } from '../../../common/platform/types'; import { IProcessServiceFactory } from '../../../common/process/types'; import { IConfigurationService } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; -import { IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../contracts'; +import { IInterpreterHelper, InterpreterType, PythonInterpreter } from '../../contracts'; import { IVirtualEnvironmentManager } from '../../virtualEnvs/types'; import { CacheableLocatorService } from './cacheableLocatorService'; @@ -14,7 +13,7 @@ import { CacheableLocatorService } from './cacheableLocatorService'; export class CurrentPathService extends CacheableLocatorService { private readonly fs: IFileSystem; public constructor(@inject(IVirtualEnvironmentManager) private virtualEnvMgr: IVirtualEnvironmentManager, - @inject(IInterpreterVersionService) private versionProvider: IInterpreterVersionService, + @inject(IInterpreterHelper) private helper: IInterpreterHelper, @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, @inject(IServiceContainer) serviceContainer: IServiceContainer) { super('CurrentPathService', serviceContainer); @@ -36,19 +35,25 @@ export class CurrentPathService extends CacheableLocatorService { .then(listOfInterpreters => _.flatten(listOfInterpreters)) .then(interpreters => interpreters.filter(item => item.length > 0)) // tslint:disable-next-line:promise-function-async - .then(interpreters => Promise.all(interpreters.map(interpreter => this.getInterpreterDetails(interpreter)))); + .then(interpreters => Promise.all(interpreters.map(interpreter => this.getInterpreterDetails(interpreter, resource)))); } - private async getInterpreterDetails(interpreter: string): Promise { + private async getInterpreterDetails(interpreter: string, resource?: Uri): Promise { return Promise.all([ - this.versionProvider.getVersion(interpreter, path.basename(interpreter)), - this.virtualEnvMgr.getEnvironmentName(interpreter) + this.helper.getInterpreterInformation(interpreter), + this.virtualEnvMgr.getEnvironmentName(interpreter), + this.virtualEnvMgr.getEnvironmentType(interpreter, resource) ]). - then(([displayName, virtualEnvName]) => { - displayName += virtualEnvName.length > 0 ? ` (${virtualEnvName})` : ''; + then(([details, virtualEnvName, type]) => { + if (!details) { + return; + } + const displayName = `${details.version ? details.version : ''}${virtualEnvName.length > 0 ? ` (${virtualEnvName})` : ''}`; return { + ...(details as PythonInterpreter), displayName, + envName: virtualEnvName, path: interpreter, - type: virtualEnvName ? InterpreterType.VirtualEnv : InterpreterType.Unknown + type: type ? type : InterpreterType.Unknown }; }); } diff --git a/src/client/interpreter/locators/services/pipEnvService.ts b/src/client/interpreter/locators/services/pipEnvService.ts index 46985a70c81d..c3e737c65955 100644 --- a/src/client/interpreter/locators/services/pipEnvService.ts +++ b/src/client/interpreter/locators/services/pipEnvService.ts @@ -9,17 +9,16 @@ import { IFileSystem } from '../../../common/platform/types'; import { IProcessServiceFactory } from '../../../common/process/types'; import { ICurrentProcess } from '../../../common/types'; import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; -import { getPythonExecutable } from '../../../debugger/Common/Utils'; import { IServiceContainer } from '../../../ioc/types'; -import { IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../contracts'; +import { IInterpreterHelper, InterpreterType, IPipEnvService, PythonInterpreter } from '../../contracts'; import { CacheableLocatorService } from './cacheableLocatorService'; const execName = 'pipenv'; const pipEnvFileNameVariable = 'PIPENV_PIPFILE'; @injectable() -export class PipEnvService extends CacheableLocatorService { - private readonly versionService: IInterpreterVersionService; +export class PipEnvService extends CacheableLocatorService implements IPipEnvService { + private readonly helper: IInterpreterHelper; private readonly processServiceFactory: IProcessServiceFactory; private readonly workspace: IWorkspaceService; private readonly fs: IFileSystem; @@ -27,7 +26,7 @@ export class PipEnvService extends CacheableLocatorService { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { super('PipEnvService', serviceContainer); - this.versionService = this.serviceContainer.get(IInterpreterVersionService); + this.helper = this.serviceContainer.get(IInterpreterHelper); this.processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); this.workspace = this.serviceContainer.get(IWorkspaceService); this.fs = this.serviceContainer.get(IFileSystem); @@ -35,6 +34,14 @@ export class PipEnvService extends CacheableLocatorService { } // tslint:disable-next-line:no-empty public dispose() { } + public async isRelatedPipEnvironment(dir: string, pythonPath: string): Promise { + // In PipEnv, the name of the cwd is used as a prefix in the virtual env. + if (pythonPath.indexOf(`${path.sep}${path.basename(dir)}-`) === -1) { + return false; + } + const envName = await this.getInterpreterPathFromPipenv(dir, true); + return !!envName; + } protected getInterpretersImplementation(resource?: Uri): Promise { const pipenvCwd = this.getPipenvWorkingDirectory(resource); if (!pipenvCwd) { @@ -52,13 +59,15 @@ export class PipEnvService extends CacheableLocatorService { return; } - const pythonExecutablePath = getPythonExecutable(interpreterPath); - const ver = await this.versionService.getVersion(pythonExecutablePath, ''); + const details = await this.helper.getInterpreterInformation(interpreterPath); + if (!details) { + return; + } return { - path: pythonExecutablePath, - displayName: `${ver} (${execName})`, - type: InterpreterType.VirtualEnv, - version: ver + ...(details as PythonInterpreter), + displayName: `${details.version} (${execName})`, + path: interpreterPath, + type: InterpreterType.PipEnv }; } @@ -72,13 +81,25 @@ export class PipEnvService extends CacheableLocatorService { return wsFolder ? wsFolder.uri.fsPath : this.workspace.rootPath; } - private async getInterpreterPathFromPipenv(cwd: string): Promise { + private async getInterpreterPathFromPipenv(cwd: string, ignoreErrors = false): Promise { // Quick check before actually running pipenv if (!await this.checkIfPipFileExists(cwd)) { return; } - const venvFolder = await this.invokePipenv('--venv', cwd); - return venvFolder && await this.fs.directoryExists(venvFolder) ? venvFolder : undefined; + try { + const pythonPath = await this.invokePipenv('--py', cwd); + // TODO: Why do we need to do this? + return pythonPath && await this.fs.fileExists(pythonPath) ? pythonPath : undefined; + // tslint:disable-next-line:no-empty + } catch (error) { + console.error(error); + if (ignoreErrors) { + return; + } + const errorMessage = error.message || error; + const appShell = this.serviceContainer.get(IApplicationShell); + appShell.showWarningMessage(`Workspace contains pipfile but attempt to run 'pipenv --py' failed with ${errorMessage}. Make sure pipenv is on the PATH.`); + } } private async checkIfPipFileExists(cwd: string): Promise { const currentProcess = this.serviceContainer.get(ICurrentProcess); diff --git a/src/client/interpreter/locators/services/windowsRegistryService.ts b/src/client/interpreter/locators/services/windowsRegistryService.ts index dc724fc7e7c8..2226fc3f387c 100644 --- a/src/client/interpreter/locators/services/windowsRegistryService.ts +++ b/src/client/interpreter/locators/services/windowsRegistryService.ts @@ -6,7 +6,7 @@ import { Uri } from 'vscode'; import { Architecture, IRegistry, RegistryHive } from '../../../common/platform/types'; import { Is64Bit } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; -import { InterpreterType, PythonInterpreter } from '../../contracts'; +import { IInterpreterHelper, InterpreterType, PythonInterpreter } from '../../contracts'; import { CacheableLocatorService } from './cacheableLocatorService'; import { AnacondaCompanyName, AnacondaCompanyNames } from './conda'; @@ -20,14 +20,14 @@ const PythonCoreCompanyDisplayName = 'Python Software Foundation'; const PythonCoreComany = 'PYTHONCORE'; type CompanyInterpreter = { - companyKey: string, - hive: RegistryHive, - arch?: Architecture + companyKey: string; + hive: RegistryHive; + arch?: Architecture; }; @injectable() export class WindowsRegistryService extends CacheableLocatorService { - constructor( @inject(IRegistry) private registry: IRegistry, + constructor(@inject(IRegistry) private registry: IRegistry, @inject(Is64Bit) private is64Bit: boolean, @inject(IServiceContainer) serviceContainer: IServiceContainer) { super('WindowsRegistryService', serviceContainer); @@ -84,11 +84,11 @@ export class WindowsRegistryService extends CacheableLocatorService { private getInreterpreterDetailsForCompany(tagKey: string, companyKey: string, hive: RegistryHive, arch?: Architecture): Promise { const key = `${tagKey}\\InstallPath`; type InterpreterInformation = null | undefined | { - installPath: string, - executablePath?: string, - displayName?: string, - version?: string, - companyDisplayName?: string + installPath: string; + executablePath?: string; + displayName?: string; + version?: string; + companyDisplayName?: string; }; return this.registry.getValue(key, hive, arch) .then(installPath => { @@ -109,20 +109,26 @@ export class WindowsRegistryService extends CacheableLocatorService { ]) .then(([installedPath, executablePath, displayName, version, companyDisplayName]) => { companyDisplayName = AnacondaCompanyNames.indexOf(companyDisplayName) === -1 ? companyDisplayName : AnacondaCompanyName; - // tslint:disable-next-line:prefer-type-cast + // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion return { installPath: installedPath, executablePath, displayName, version, companyDisplayName } as InterpreterInformation; }); }) - .then((interpreterInfo?: InterpreterInformation) => { + .then(async (interpreterInfo?: InterpreterInformation) => { if (!interpreterInfo) { return; } const executablePath = interpreterInfo.executablePath && interpreterInfo.executablePath.length > 0 ? interpreterInfo.executablePath : path.join(interpreterInfo.installPath, DefaultPythonExecutable); const displayName = interpreterInfo.displayName; - const version = interpreterInfo.version ? path.basename(interpreterInfo.version) : path.basename(tagKey); - // tslint:disable-next-line:prefer-type-cast + const helper = this.serviceContainer.get(IInterpreterHelper); + const details = await helper.getInterpreterInformation(executablePath); + if (!details) { + return; + } + const version = interpreterInfo.version ? path.basename(interpreterInfo.version) : details.version; + // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion return { + ...(details as PythonInterpreter), architecture: arch, displayName, path: executablePath, diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 18b891608f26..f867bb2ce1ac 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -20,6 +20,7 @@ import { IInterpreterVersionService, IKnownSearchPathsForInterpreters, INTERPRETER_LOCATOR_SERVICE, + IPipEnvService, IShebangCodeLensProvider, IVirtualEnvironmentsSearchPathProvider, KNOWN_PATH_SERVICE, @@ -61,6 +62,7 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IInterpreterLocatorService, GlobalVirtualEnvService, GLOBAL_VIRTUAL_ENV_SERVICE); serviceManager.addSingleton(IInterpreterLocatorService, WorkspaceVirtualEnvService, WORKSPACE_VIRTUAL_ENV_SERVICE); serviceManager.addSingleton(IInterpreterLocatorService, PipEnvService, PIPENV_SERVICE); + serviceManager.addSingleton(IPipEnvService, PipEnvService); const isWindows = serviceManager.get(IsWindows); if (isWindows) { diff --git a/src/client/interpreter/virtualEnvs/index.ts b/src/client/interpreter/virtualEnvs/index.ts index bf05b6a3b917..61f14d7f1c6f 100644 --- a/src/client/interpreter/virtualEnvs/index.ts +++ b/src/client/interpreter/virtualEnvs/index.ts @@ -2,26 +2,84 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../common/application/types'; +import { IFileSystem } from '../../common/platform/types'; import { IProcessServiceFactory } from '../../common/process/types'; import { IServiceContainer } from '../../ioc/types'; +import { InterpreterType, IPipEnvService } from '../contracts'; import { IVirtualEnvironmentManager } from './types'; +const PYENVFILES = ['pyvenv.cfg', path.join('..', 'pyvenv.cfg')]; + @injectable() export class VirtualEnvironmentManager implements IVirtualEnvironmentManager { private processServiceFactory: IProcessServiceFactory; + private pipEnvService: IPipEnvService; + private fs: IFileSystem; + private pyEnvRoot?: string; + private workspaceService: IWorkspaceService; constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); + this.fs = serviceContainer.get(IFileSystem); + this.pipEnvService = serviceContainer.get(IPipEnvService); + this.workspaceService = serviceContainer.get(IWorkspaceService); } public async getEnvironmentName(pythonPath: string): Promise { // https://stackoverflow.com/questions/1871549/determine-if-python-is-running-inside-virtualenv // hasattr(sys, 'real_prefix') works for virtualenv while // '(hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix))' works for venv - const code = 'import sys\nif hasattr(sys, "real_prefix"):\n print("virtualenv")\nelif hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix:\n print("venv")'; - const processService = await this.processServiceFactory.create(); - const output = await processService.exec(pythonPath, ['-c', code]); - if (output.stdout.length > 0) { - return output.stdout.trim(); + try { + const processService = await this.processServiceFactory.create(); + const code = 'import sys\nif hasattr(sys, "real_prefix"):\n print("virtualenv")\nelif hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix:\n print("venv")'; + const output = await processService.exec(pythonPath, ['-c', code]); + if (output.stdout.length > 0) { + return output.stdout.trim(); + } + } catch { + // do nothing. } return ''; } + public async getEnvironmentType(pythonPath: string, resource?: Uri): Promise { + const dir = path.dirname(pythonPath); + const pyEnvCfgFiles = PYENVFILES.map(file => path.join(dir, file)); + for (const file of pyEnvCfgFiles) { + if (await this.fs.fileExists(file)) { + return InterpreterType.Venv; + } + } + + const pyEnvRoot = await this.getPyEnvRoot(resource); + if (pyEnvRoot && pythonPath.startsWith(pyEnvRoot)) { + return InterpreterType.Pyenv; + } + + const defaultWorkspaceUri = this.workspaceService.hasWorkspaceFolders ? this.workspaceService.workspaceFolders![0].uri : undefined; + const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; + const workspaceUri = workspaceFolder ? workspaceFolder.uri : defaultWorkspaceUri; + if (workspaceUri && this.pipEnvService.isRelatedPipEnvironment(pythonPath, workspaceUri.fsPath)) { + return InterpreterType.PipEnv; + } + + if ((await this.getEnvironmentName(pythonPath)).length > 0) { + return InterpreterType.VirtualEnv; + } + + // Lets not try to determine whether this is a conda environment or not. + return InterpreterType.Unknown; + } + private async getPyEnvRoot(resource?: Uri): Promise { + if (this.pyEnvRoot) { + return this.pyEnvRoot; + } + try { + const processService = await this.processServiceFactory.create(resource); + const output = await processService.exec('pyenv', ['root']); + return this.pyEnvRoot = output.stdout.trim(); + } catch { + return; + } + } } diff --git a/src/client/interpreter/virtualEnvs/types.ts b/src/client/interpreter/virtualEnvs/types.ts index 971772fd009d..6096f88d374c 100644 --- a/src/client/interpreter/virtualEnvs/types.ts +++ b/src/client/interpreter/virtualEnvs/types.ts @@ -1,7 +1,11 @@ + // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { Uri } from 'vscode'; +import { InterpreterType } from '../contracts'; export const IVirtualEnvironmentManager = Symbol('VirtualEnvironmentManager'); export interface IVirtualEnvironmentManager { getEnvironmentName(pythonPath: string): Promise; + getEnvironmentType(pythonPath: string, resource?: Uri): Promise; } diff --git a/src/client/linters/errorHandlers/notInstalled.ts b/src/client/linters/errorHandlers/notInstalled.ts index 3ffd90363c58..c3fbd9447296 100644 --- a/src/client/linters/errorHandlers/notInstalled.ts +++ b/src/client/linters/errorHandlers/notInstalled.ts @@ -11,10 +11,10 @@ export class NotInstalledErrorHandler extends BaseErrorHandler { super(product, outputChannel, serviceContainer); } public async handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { - const pythonExecutionService = await this.serviceContainer.get(IPythonExecutionFactory).create(resource); + const pythonExecutionService = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource }); const isModuleInstalled = await pythonExecutionService.isModuleInstalled(execInfo.moduleName!); if (isModuleInstalled) { - return this.nextHandler ? await this.nextHandler.handleError(error, resource, execInfo) : false; + return this.nextHandler ? this.nextHandler.handleError(error, resource, execInfo) : false; } this.installer.promptToInstall(this.product, resource) diff --git a/src/client/providers/importSortProvider.ts b/src/client/providers/importSortProvider.ts index 74effd123947..90733f15bfe1 100644 --- a/src/client/providers/importSortProvider.ts +++ b/src/client/providers/importSortProvider.ts @@ -38,7 +38,7 @@ export class PythonImportSortProvider { const processService = await this.processServiceFactory.create(document.uri); promise = processService.exec(isort, args, { throwOnStdErr: true }); } else { - promise = this.pythonExecutionFactory.create(document.uri) + promise = this.pythonExecutionFactory.create({ resource: document.uri }) .then(executionService => executionService.exec([importScript].concat(args), { throwOnStdErr: true })); } diff --git a/src/client/providers/jediProxy.ts b/src/client/providers/jediProxy.ts index 85714dd9cf50..8d7811e161b8 100644 --- a/src/client/providers/jediProxy.ts +++ b/src/client/providers/jediProxy.ts @@ -330,7 +330,7 @@ export class JediProxy implements Disposable { this.languageServerStarted.reject(new Error('Language Server not started.')); } this.languageServerStarted = createDeferred(); - const pythonProcess = await this.serviceContainer.get(IPythonExecutionFactory).create(Uri.file(this.workspacePath)); + const pythonProcess = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource: Uri.file(this.workspacePath) }); // Check if the python path is valid. if ((await pythonProcess.getExecutablePath().catch(() => '')).length === 0) { return; @@ -606,7 +606,7 @@ export class JediProxy implements Disposable { private async getPathFromPythonCommand(args: string[]): Promise { try { - const pythonProcess = await this.serviceContainer.get(IPythonExecutionFactory).create(Uri.file(this.workspacePath)); + const pythonProcess = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource: Uri.file(this.workspacePath) }); const result = await pythonProcess.exec(args, { cwd: this.workspacePath }); const lines = result.stdout.trim().splitLines(); if (lines.length === 0) { diff --git a/src/client/refactor/proxy.ts b/src/client/refactor/proxy.ts index 725f5eb65005..d5268d1f931f 100644 --- a/src/client/refactor/proxy.ts +++ b/src/client/refactor/proxy.ts @@ -106,7 +106,7 @@ export class RefactorProxy extends Disposable { }); } private async initialize(pythonPath: string): Promise { - const pythonProc = await this.serviceContainer.get(IPythonExecutionFactory).create(Uri.file(this.workspaceRoot)); + const pythonProc = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource: Uri.file(this.workspaceRoot) }); this.initialized = createDeferred(); const args = ['refactor.py', this.workspaceRoot]; const cwd = path.join(this._extensionDir, 'pythonFiles'); diff --git a/src/client/unittests/common/runner.ts b/src/client/unittests/common/runner.ts index 56f7fba07eab..9bf612a33b58 100644 --- a/src/client/unittests/common/runner.ts +++ b/src/client/unittests/common/runner.ts @@ -3,15 +3,14 @@ import { CancellationToken, OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import { ErrorUtils } from '../../common/errors/errorUtils'; import { ModuleNotInstalledError } from '../../common/errors/moduleNotInstalledError'; -import { IPythonToolExecutionService } from '../../common/process/types'; import { IPythonExecutionFactory, IPythonExecutionService, + IPythonToolExecutionService, ObservableExecutionResult, SpawnOptions } from '../../common/process/types'; -import { IPythonSettings } from '../../common/types'; -import { ExecutionInfo } from '../../common/types'; +import { ExecutionInfo, IPythonSettings } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { NOSETEST_PROVIDER, PYTEST_PROVIDER, UNITTEST_PROVIDER } from './constants'; import { ITestsHelper, TestProvider } from './types'; @@ -36,7 +35,7 @@ export async function run(serviceContainer: IServiceContainer, testProvider: Tes if (!testExecutablePath && testProvider === UNITTEST_PROVIDER) { // Unit tests have a special way of being executed const pythonServiceFactory = serviceContainer.get(IPythonExecutionFactory); - pythonExecutionServicePromise = pythonServiceFactory.create(options.workspaceFolder); + pythonExecutionServicePromise = pythonServiceFactory.create({ resource: options.workspaceFolder }); promise = pythonExecutionServicePromise.then(executionService => executionService.execObservable(options.args, { ...spawnOptions })); } else { const pythonToolsExecutionService = serviceContainer.get(IPythonToolExecutionService); diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index 84835f18f7b2..3ee2379fcc7e 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -27,6 +27,19 @@ import { MockProcessService } from '../mocks/proc'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; import { closeActiveWindows, initializeTest } from './../initialize'; +const info: PythonInterpreter = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + type: InterpreterType.Unknown, + version: '', + version_info: [0, 0, 0, 'alpha'], + sysPrefix: '', + sysVersion: '' +}; + // tslint:disable-next-line:max-func-body-length suite('Module Installer', () => { let ioc: UnitTestIocContainer; @@ -91,7 +104,7 @@ suite('Module Installer', () => { async function getCurrentPythonPath(): Promise { const pythonPath = PythonSettings.getInstance(workspaceUri).pythonPath; if (path.basename(pythonPath) === pythonPath) { - const pythonProc = await ioc.serviceContainer.get(IPythonExecutionFactory).create(workspaceUri); + const pythonProc = await ioc.serviceContainer.get(IPythonExecutionFactory).create({ resource: workspaceUri }); return pythonProc.getExecutablePath().catch(() => pythonPath); } else { return pythonPath; @@ -133,7 +146,7 @@ suite('Module Installer', () => { ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('mock', true)); const pythonPath = await getCurrentPythonPath(); const mockInterpreterLocator = TypeMoq.Mock.ofType(); - mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([{ architecture: Architecture.Unknown, companyDisplayName: '', displayName: '', envName: '', path: pythonPath, type: InterpreterType.Conda, version: '' }])); + mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([{ ...info, architecture: Architecture.Unknown, companyDisplayName: '', displayName: '', envName: '', path: pythonPath, type: InterpreterType.Conda, version: '' }])); ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, TypeMoq.Mock.ofType().object, PIPENV_SERVICE); @@ -189,11 +202,12 @@ suite('Module Installer', () => { test('Validate pip install arguments', async () => { const interpreterPath = await getCurrentPythonPath(); const mockInterpreterLocator = TypeMoq.Mock.ofType(); - mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([{ path: interpreterPath, type: InterpreterType.Unknown }])); + mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([{ ...info, path: interpreterPath, type: InterpreterType.Unknown }])); ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, TypeMoq.Mock.ofType().object, PIPENV_SERVICE); const interpreter: PythonInterpreter = { + ...info, type: InterpreterType.Unknown, path: PYTHON_PATH }; @@ -220,7 +234,7 @@ suite('Module Installer', () => { test('Validate Conda install arguments', async () => { const interpreterPath = await getCurrentPythonPath(); const mockInterpreterLocator = TypeMoq.Mock.ofType(); - mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([{ path: interpreterPath, type: InterpreterType.Conda }])); + mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([{ ...info, path: interpreterPath, type: InterpreterType.Conda }])); ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, TypeMoq.Mock.ofType().object, PIPENV_SERVICE); @@ -242,7 +256,7 @@ suite('Module Installer', () => { test('Validate pipenv install arguments', async () => { const mockInterpreterLocator = TypeMoq.Mock.ofType(); - mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([{ path: 'interpreterPath', type: InterpreterType.VirtualEnv }])); + mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([{ ...info, path: 'interpreterPath', type: InterpreterType.VirtualEnv }])); ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator.object, PIPENV_SERVICE); const moduleName = 'xyz'; diff --git a/src/test/common/process/pythonProc.simple.multiroot.test.ts b/src/test/common/process/pythonProc.simple.multiroot.test.ts index d8ff7c09cbb4..c0c914ec7982 100644 --- a/src/test/common/process/pythonProc.simple.multiroot.test.ts +++ b/src/test/common/process/pythonProc.simple.multiroot.test.ts @@ -83,7 +83,7 @@ suite('PythonExecutableService', () => { test('Importing without a valid PYTHONPATH should fail', async () => { await configService.updateSettingAsync('envFile', 'someInvalidFile.env', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); pythonExecFactory = serviceContainer.get(IPythonExecutionFactory); - const pythonExecService = await pythonExecFactory.create(workspace4PyFile); + const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); const promise = pythonExecService.exec([workspace4PyFile.fsPath], { cwd: path.dirname(workspace4PyFile.fsPath), throwOnStdErr: true }); await expect(promise).to.eventually.be.rejectedWith(StdErrError); @@ -91,14 +91,14 @@ suite('PythonExecutableService', () => { test('Importing with a valid PYTHONPATH from .env file should succeed', async () => { await configService.updateSettingAsync('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); - const pythonExecService = await pythonExecFactory.create(workspace4PyFile); + const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); const promise = pythonExecService.exec([workspace4PyFile.fsPath], { cwd: path.dirname(workspace4PyFile.fsPath), throwOnStdErr: true }); await expect(promise).to.eventually.have.property('stdout', `Hello${EOL}`); }); test('Known modules such as \'os\' and \'sys\' should be deemed \'installed\'', async () => { - const pythonExecService = await pythonExecFactory.create(workspace4PyFile); + const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); const osModuleIsInstalled = pythonExecService.isModuleInstalled('os'); const sysModuleIsInstalled = pythonExecService.isModuleInstalled('sys'); await expect(osModuleIsInstalled).to.eventually.equal(true, 'os module is not installed'); @@ -106,7 +106,7 @@ suite('PythonExecutableService', () => { }); test('Unknown modules such as \'xyzabc123\' be deemed \'not installed\'', async () => { - const pythonExecService = await pythonExecFactory.create(workspace4PyFile); + const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); const randomModuleName = `xyz123${new Date().getSeconds()}`; const randomModuleIsInstalled = pythonExecService.isModuleInstalled(randomModuleName); await expect(randomModuleIsInstalled).to.eventually.equal(false, `Random module '${randomModuleName}' is installed`); @@ -119,7 +119,7 @@ suite('PythonExecutableService', () => { resolve(stdout.trim()); }); }); - const pythonExecService = await pythonExecFactory.create(workspace4PyFile); + const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); const executablePath = await pythonExecService.getExecutablePath(); expect(executablePath).to.equal(expectedExecutablePath, 'Executable paths are not the same'); }); diff --git a/src/test/configuration/interpreterSelector.test.ts b/src/test/configuration/interpreterSelector.test.ts index 98ea1fd27cc9..9d222f5e1df6 100644 --- a/src/test/configuration/interpreterSelector.test.ts +++ b/src/test/configuration/interpreterSelector.test.ts @@ -5,13 +5,26 @@ import * as assert from 'assert'; import { Container } from 'inversify'; import * as TypeMoq from 'typemoq'; import { IApplicationShell, ICommandManager, IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; -import { IFileSystem } from '../../client/common/platform/types'; +import { Architecture, IFileSystem } from '../../client/common/platform/types'; import { IInterpreterQuickPickItem, InterpreterSelector } from '../../client/interpreter/configuration/interpreterSelector'; import { IInterpreterService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceContainer } from '../../client/ioc/types'; +const info: PythonInterpreter = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + type: InterpreterType.Unknown, + version: '', + version_info: [0, 0, 0, 'alpha'], + sysPrefix: '', + sysVersion: '' +}; + class InterpreterQuickPickItem implements IInterpreterQuickPickItem { public path: string; public label: string; @@ -73,7 +86,7 @@ suite('Interpreters - selector', () => { { displayName: '2 (virtualenv)', path: 'c:/path2/path2', type: InterpreterType.VirtualEnv }, { displayName: '3', path: 'c:/path2/path2', type: InterpreterType.Unknown }, { displayName: '4', path: 'c:/path4/path4', type: InterpreterType.Conda } - ]; + ].map(item => { return { ...info, ...item }; }); interpreterService .setup(x => x.getInterpreters(TypeMoq.It.isAny())) .returns(() => new Promise((resolve) => resolve(initial))); diff --git a/src/test/format/extension.format.test.ts b/src/test/format/extension.format.test.ts index 503de9c117c2..db99ec32de41 100644 --- a/src/test/format/extension.format.test.ts +++ b/src/test/format/extension.format.test.ts @@ -36,7 +36,7 @@ suite('Formatting', () => { fs.copySync(originalUnformattedFile, file, { overwrite: true }); }); fs.ensureDirSync(path.dirname(autoPep8FileToFormat)); - const pythonProcess = await ioc.serviceContainer.get(IPythonExecutionFactory).create(vscode.Uri.file(workspaceRootPath)); + const pythonProcess = await ioc.serviceContainer.get(IPythonExecutionFactory).create({ resource: vscode.Uri.file(workspaceRootPath) }); const py2 = await ioc.getPythonMajorVersion(vscode.Uri.parse(originalUnformattedFile)) === 2; const yapf = pythonProcess.execModule('yapf', [originalUnformattedFile], { cwd: workspaceRootPath }); const autoPep8 = pythonProcess.execModule('autopep8', [originalUnformattedFile], { cwd: workspaceRootPath }); @@ -113,7 +113,7 @@ suite('Formatting', () => { } test('AutoPep8', async () => testFormatting(new AutoPep8Formatter(ioc.serviceContainer), formattedAutoPep8, autoPep8FileToFormat, 'autopep8.output')); - test('Black', async function() { + test('Black', async function () { if (await ioc.getPythonMajorVersion(vscode.Uri.parse(blackFileToFormat)) === 2) { // tslint:disable-next-line:no-invalid-this return this.skip(); diff --git a/src/test/install/channelManager.channels.test.ts b/src/test/install/channelManager.channels.test.ts index 714f9c65a8f8..3662859ee1fc 100644 --- a/src/test/install/channelManager.channels.test.ts +++ b/src/test/install/channelManager.channels.test.ts @@ -8,12 +8,26 @@ import { QuickPickOptions } from 'vscode'; import { IApplicationShell } from '../../client/common/application/types'; import { InstallationChannelManager } from '../../client/common/installer/channelManager'; import { IModuleInstaller } from '../../client/common/installer/types'; +import { Architecture } from '../../client/common/platform/types'; import { Product } from '../../client/common/types'; import { IInterpreterLocatorService, InterpreterType, PIPENV_SERVICE, PythonInterpreter } from '../../client/interpreter/contracts'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceContainer } from '../../client/ioc/types'; +const info: PythonInterpreter = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + type: InterpreterType.Unknown, + version: '', + version_info: [0, 0, 0, 'alpha'], + sysPrefix: '', + sysVersion: '' +}; + // tslint:disable-next-line:max-func-body-length suite('Installation - installation channels', () => { let serviceManager: ServiceManager; @@ -55,6 +69,7 @@ suite('Installation - installation channels', () => { const pipenvInstaller = mockInstaller(true, 'pipenv', 10); const interpreter: PythonInterpreter = { + ...info, path: 'pipenv', type: InterpreterType.VirtualEnv }; diff --git a/src/test/install/channelManager.messages.test.ts b/src/test/install/channelManager.messages.test.ts index 0762b4242014..7d332917a0f1 100644 --- a/src/test/install/channelManager.messages.test.ts +++ b/src/test/install/channelManager.messages.test.ts @@ -6,13 +6,26 @@ import { Container } from 'inversify'; import * as TypeMoq from 'typemoq'; import { IApplicationShell } from '../../client/common/application/types'; import { InstallationChannelManager } from '../../client/common/installer/channelManager'; -import { IPlatformService } from '../../client/common/platform/types'; +import { Architecture, IPlatformService } from '../../client/common/platform/types'; import { Product } from '../../client/common/types'; import { IInterpreterService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceContainer } from '../../client/ioc/types'; +const info: PythonInterpreter = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + type: InterpreterType.Unknown, + version: '', + version_info: [0, 0, 0, 'alpha'], + sysPrefix: '', + sysVersion: '' +}; + // tslint:disable-next-line:max-func-body-length suite('Installation - channel messages', () => { let serviceContainer: IServiceContainer; @@ -132,6 +145,7 @@ suite('Installation - channel messages', () => { verify: (c: InstallationChannelManager, m: string, u: string) => void): Promise { const activeInterpreter: PythonInterpreter = { + ...info, type: interpreterType, path: '' }; diff --git a/src/test/install/pythonInstallation.test.ts b/src/test/install/pythonInstallation.test.ts index 23e5429c1bb9..b6131cc9c077 100644 --- a/src/test/install/pythonInstallation.test.ts +++ b/src/test/install/pythonInstallation.test.ts @@ -7,15 +7,27 @@ import { Container } from 'inversify'; import * as TypeMoq from 'typemoq'; import { IApplicationShell } from '../../client/common/application/types'; import { PythonInstaller } from '../../client/common/installer/pythonInstallation'; -import { IPlatformService } from '../../client/common/platform/types'; +import { Architecture, IPlatformService } from '../../client/common/platform/types'; import { IPythonSettings } from '../../client/common/types'; -import { IInterpreterLocatorService, IInterpreterService } from '../../client/interpreter/contracts'; -import { InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; +import { IInterpreterLocatorService, IInterpreterService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceContainer } from '../../client/ioc/types'; import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +const info: PythonInterpreter = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + type: InterpreterType.Unknown, + version: '', + version_info: [0, 0, 0, 'alpha'], + sysPrefix: '', + sysVersion: '' +}; + class TestContext { public serviceManager: ServiceManager; public serviceContainer: IServiceContainer; @@ -36,6 +48,7 @@ class TestContext { this.settings = TypeMoq.Mock.ofType(); const activeInterpreter: PythonInterpreter = { + ...info, type: InterpreterType.Unknown, path: '' }; @@ -116,6 +129,7 @@ suite('Installation', () => { c.appShell.setup(x => x.showWarningMessage(TypeMoq.It.isAnyString())).callback(() => called = true); c.settings.setup(x => x.pythonPath).returns(() => 'python'); const interpreter: PythonInterpreter = { + ...info, path: 'python', type: InterpreterType.Unknown }; diff --git a/src/test/interpreters/condaEnvFileService.test.ts b/src/test/interpreters/condaEnvFileService.test.ts index 92783ddf3bc9..062722354c90 100644 --- a/src/test/interpreters/condaEnvFileService.test.ts +++ b/src/test/interpreters/condaEnvFileService.test.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { IFileSystem } from '../../client/common/platform/types'; import { ILogger, IPersistentStateFactory } from '../../client/common/types'; -import { ICondaService, IInterpreterLocatorService, IInterpreterVersionService, InterpreterType } from '../../client/interpreter/contracts'; +import { ICondaService, IInterpreterHelper, IInterpreterLocatorService, InterpreterType } from '../../client/interpreter/contracts'; import { AnacondaCompanyName, AnacondaCompanyNames, AnacondaDisplayName } from '../../client/interpreter/locators/services/conda'; import { CondaEnvFileService } from '../../client/interpreter/locators/services/condaEnvFileService'; import { IServiceContainer } from '../../client/ioc/types'; @@ -18,7 +18,7 @@ const environmentsFilePath = path.join(environmentsPath, 'environments.txt'); suite('Interpreters from Conda Environments Text File', () => { let logger: TypeMoq.IMock; let condaService: TypeMoq.IMock; - let interpreterVersion: TypeMoq.IMock; + let interpreterHelper: TypeMoq.IMock; let condaFileProvider: IInterpreterLocatorService; let fileSystem: TypeMoq.IMock; suiteSetup(initialize); @@ -31,14 +31,14 @@ suite('Interpreters from Conda Environments Text File', () => { stateFactory.setup(s => s.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => state); condaService = TypeMoq.Mock.ofType(); - interpreterVersion = TypeMoq.Mock.ofType(); + interpreterHelper = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); logger = TypeMoq.Mock.ofType(); - condaFileProvider = new CondaEnvFileService(interpreterVersion.object, condaService.object, fileSystem.object, serviceContainer.object, logger.object); + condaFileProvider = new CondaEnvFileService(interpreterHelper.object, condaService.object, fileSystem.object, serviceContainer.object, logger.object); }); test('Must return an empty list if environment file cannot be found', async () => { condaService.setup(c => c.condaEnvironmentsFile).returns(() => undefined); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('Mock Name')); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: 'Mock Name' })); const interpreters = await condaFileProvider.getInterpreters(); assert.equal(interpreters.length, 0, 'Incorrect number of entries'); }); @@ -46,7 +46,7 @@ suite('Interpreters from Conda Environments Text File', () => { condaService.setup(c => c.condaEnvironmentsFile).returns(() => environmentsFilePath); fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(environmentsFilePath))).returns(() => Promise.resolve(true)); fileSystem.setup(fs => fs.readFile(TypeMoq.It.isValue(environmentsFilePath))).returns(() => Promise.resolve('')); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('Mock Name')); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: 'Mock Name' })); const interpreters = await condaFileProvider.getInterpreters(); assert.equal(interpreters.length, 0, 'Incorrect number of entries'); }); @@ -81,7 +81,7 @@ suite('Interpreters from Conda Environments Text File', () => { }); fileSystem.setup(fs => fs.readFile(TypeMoq.It.isValue(environmentsFilePath))).returns(() => Promise.resolve(interpreterPaths.join(EOL))); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('Mock Name')); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: 'Mock Name' })); const interpreters = await condaFileProvider.getInterpreters(); @@ -113,7 +113,7 @@ suite('Interpreters from Conda Environments Text File', () => { for (const companyName of AnacondaCompanyNames) { const versionWithCompanyName = `Mock Version :: ${companyName}`; - interpreterVersion.setup(c => c.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(versionWithCompanyName)); + interpreterHelper.setup(c => c.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: versionWithCompanyName })); const interpreters = await condaFileProvider.getInterpreters(); assert.equal(interpreters.length, 1, 'Incorrect number of entries'); diff --git a/src/test/interpreters/condaEnvService.test.ts b/src/test/interpreters/condaEnvService.test.ts index 82da233fb9e9..2523ce3e6a0a 100644 --- a/src/test/interpreters/condaEnvService.test.ts +++ b/src/test/interpreters/condaEnvService.test.ts @@ -3,7 +3,8 @@ import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { IFileSystem } from '../../client/common/platform/types'; import { ILogger, IPersistentStateFactory } from '../../client/common/types'; -import { ICondaService, IInterpreterVersionService, InterpreterType } from '../../client/interpreter/contracts'; +import { ICondaService, InterpreterType } from '../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../client/interpreter/helpers'; import { AnacondaCompanyName, AnacondaDisplayName } from '../../client/interpreter/locators/services/conda'; import { CondaEnvService } from '../../client/interpreter/locators/services/condaEnvService'; import { IServiceContainer } from '../../client/ioc/types'; @@ -14,12 +15,12 @@ import { MockState } from './mocks'; const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); // tslint:disable-next-line:max-func-body-length -suite('Interpreters from Conda Environments', () => { +suite('Interpreters from Conda Environmentsx', () => { let ioc: UnitTestIocContainer; let logger: TypeMoq.IMock; let condaProvider: CondaEnvService; let condaService: TypeMoq.IMock; - let interpreterVersion: TypeMoq.IMock; + let interpreterHelper: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; suiteSetup(initialize); setup(async () => { @@ -32,9 +33,9 @@ suite('Interpreters from Conda Environments', () => { stateFactory.setup(s => s.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => state); condaService = TypeMoq.Mock.ofType(); - interpreterVersion = TypeMoq.Mock.ofType(); + interpreterHelper = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); - condaProvider = new CondaEnvService(condaService.object, interpreterVersion.object, logger.object, serviceContainer.object, fileSystem.object); + condaProvider = new CondaEnvService(condaService.object, interpreterHelper.object, logger.object, serviceContainer.object, fileSystem.object); }); teardown(() => ioc.dispose()); function initializeDI() { @@ -65,7 +66,7 @@ suite('Interpreters from Conda Environments', () => { const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); }); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: '' })); const interpreters = await condaProvider.parseCondaInfo(info); assert.equal(interpreters.length, 2, 'Incorrect number of entries'); @@ -102,7 +103,7 @@ suite('Interpreters from Conda Environments', () => { const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); }); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: '' })); condaService.setup(c => c.getCondaFile()).returns(() => Promise.resolve('conda')); condaService.setup(c => c.getCondaInfo()).returns(() => Promise.resolve(info)); condaService.setup(c => c.getCondaEnvironments(TypeMoq.It.isAny())).returns(() => Promise.resolve([ @@ -147,7 +148,7 @@ suite('Interpreters from Conda Environments', () => { const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); }); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: '' })); const interpreters = await condaProvider.parseCondaInfo(info); assert.equal(interpreters.length, 1, 'Incorrect number of entries'); @@ -171,7 +172,7 @@ suite('Interpreters from Conda Environments', () => { default_prefix: '', 'sys.version': '3.6.1 |Anaonda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]' }; - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: '' })); condaService.setup(c => c.getCondaInfo()).returns(() => Promise.resolve(info)); condaService.setup(c => c.getCondaEnvironments(TypeMoq.It.isAny())).returns(() => Promise.resolve([ { name: 'base', path: environmentsPath }, @@ -185,7 +186,7 @@ suite('Interpreters from Conda Environments', () => { const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); }); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); fileSystem.setup(fs => fs.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((p1: string, p2: string) => isWindows ? p1 === p2 : p1.toUpperCase() === p2.toUpperCase()); const interpreters = await condaProvider.getInterpreters(); @@ -215,7 +216,7 @@ suite('Interpreters from Conda Environments', () => { const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); }); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: '' })); const interpreters = await condaProvider.parseCondaInfo(info); assert.equal(interpreters.length, 1, 'Incorrect number of entries'); @@ -245,7 +246,7 @@ suite('Interpreters from Conda Environments', () => { const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); }); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: '' })); condaService.setup(c => c.getCondaFile()).returns(() => Promise.resolve('conda')); condaService.setup(c => c.getCondaInfo()).returns(() => Promise.resolve(info)); condaService.setup(c => c.getCondaEnvironments(TypeMoq.It.isAny())).returns(() => Promise.resolve([ @@ -280,7 +281,7 @@ suite('Interpreters from Conda Environments', () => { }); const pythonPath = isWindows ? path.join(info.default_prefix, 'python.exe') : path.join(info.default_prefix, 'bin', 'python'); fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: '' })); const interpreters = await condaProvider.parseCondaInfo(info); assert.equal(interpreters.length, 1, 'Incorrect number of entries'); @@ -308,7 +309,7 @@ suite('Interpreters from Conda Environments', () => { path.join(environmentsPath, 'path3', 'three.exe')] }; const validPaths = info.envs.filter((_, index) => index % 2 === 0); - interpreterVersion.setup(i => i.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_p, defaultValue) => Promise.resolve(defaultValue)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: '' })); validPaths.forEach(envPath => { condaService.setup(c => c.getInterpreterPath(TypeMoq.It.isValue(envPath))).returns(environmentPath => { return isWindows ? path.join(environmentPath, 'python.exe') : path.join(environmentPath, 'bin', 'python'); diff --git a/src/test/interpreters/condaService.test.ts b/src/test/interpreters/condaService.test.ts index cbcdcf5642da..10a3db6c6fc0 100644 --- a/src/test/interpreters/condaService.test.ts +++ b/src/test/interpreters/condaService.test.ts @@ -5,7 +5,7 @@ import { EOL } from 'os'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { FileSystem } from '../../client/common/platform/fileSystem'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; +import { Architecture, IFileSystem, IPlatformService } from '../../client/common/platform/types'; import { IProcessService, IProcessServiceFactory } from '../../client/common/process/types'; import { ILogger, IPersistentStateFactory } from '../../client/common/types'; import { IInterpreterLocatorService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; @@ -16,6 +16,18 @@ import { MockState } from './mocks'; const untildify: (value: string) => string = require('untildify'); const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); +const info: PythonInterpreter = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + type: InterpreterType.Unknown, + version: '', + version_info: [0, 0, 0, 'alpha'], + sysPrefix: '', + sysVersion: '' +}; suite('Interpreters Conda Service', () => { let processService: TypeMoq.IMock; @@ -256,10 +268,12 @@ suite('Interpreters Conda Service', () => { const condaPythonExePath = path.join('dumyPath', 'environments', 'conda', 'Scripts', 'python.exe'); const registryInterpreters: PythonInterpreter[] = [ { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1', type: InterpreterType.Unknown }, - { displayName: 'Anaconda', path: condaPythonExePath, companyDisplayName: 'Two 2', version: '1.11.0', type: InterpreterType.Unknown }, + { displayName: 'Anaconda', path: condaPythonExePath, companyDisplayName: 'Two 2', version: '1.11.0', enyTpe: InterpreterType.Unknown }, { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1', type: InterpreterType.Unknown }, { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } - ]; + ].map(item => { + return { ...info, ...item }; + }); const condaInterpreterIndex = registryInterpreters.findIndex(i => i.displayName === 'Anaconda'); const expectedCodnaPath = path.join(path.dirname(registryInterpreters[condaInterpreterIndex].path), 'conda.exe'); platformService.setup(p => p.isWindows).returns(() => true); @@ -281,7 +295,9 @@ suite('Interpreters Conda Service', () => { { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda221', 'Scripts', 'python.exe'), companyDisplayName: 'Two 2.21', version: '2.21.0', type: InterpreterType.Unknown }, { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1', type: InterpreterType.Unknown }, { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } - ]; + ].map(item => { + return { ...info, ...item }; + }); const indexOfLatestVersion = 3; const expectedCodnaPath = path.join(path.dirname(registryInterpreters[indexOfLatestVersion].path), 'conda.exe'); platformService.setup(p => p.isWindows).returns(() => true); @@ -303,7 +319,7 @@ suite('Interpreters Conda Service', () => { { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda221', 'Scripts', 'python.exe'), companyDisplayName: 'Two 2.21', version: '2.21.0', type: InterpreterType.Unknown }, { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1', type: InterpreterType.Unknown }, { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } - ]; + ].map(item => { return { ...info, ...item }; }); platformService.setup(p => p.isWindows).returns(() => true); processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); registryInterpreterLocatorService.setup(r => r.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(registryInterpreters)); @@ -375,17 +391,17 @@ suite('Interpreters Conda Service', () => { }); test('Returns condaInfo when conda exists', async () => { - const info = { + const expectedInfo = { envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy'), path.join(environmentsPath, 'conda', 'envs', 'scipy')], default_prefix: '', 'sys.version': '3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]' }; processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: 'xyz' })); - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['info', '--json']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: JSON.stringify(info) })); + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['info', '--json']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: JSON.stringify(expectedInfo) })); const condaInfo = await condaService.getCondaInfo(); - assert.deepEqual(condaInfo, info, 'Conda info does not match'); + assert.deepEqual(condaInfo, expectedInfo, 'Conda info does not match'); }); test('Returns undefined if there\'s and error in getting the info', async () => { @@ -464,7 +480,9 @@ suite('Interpreters Conda Service', () => { { displayName: 'Anaconda', path: condaPythonExePath, companyDisplayName: 'Two 2', version: '1.11.0', type: InterpreterType.Unknown }, { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1', type: InterpreterType.Unknown }, { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } - ]; + ].map(item => { + return { ...info, ...item }; + }); const expectedCodaExe = path.join(path.dirname(condaPythonExePath), 'conda.exe'); diff --git a/src/test/interpreters/currentPathService.test.ts b/src/test/interpreters/currentPathService.test.ts index dec8298fb205..87949998b5e6 100644 --- a/src/test/interpreters/currentPathService.test.ts +++ b/src/test/interpreters/currentPathService.test.ts @@ -11,6 +11,7 @@ import { IFileSystem } from '../../client/common/platform/types'; import { IProcessService, IProcessServiceFactory } from '../../client/common/process/types'; import { IConfigurationService, IPersistentState, IPersistentStateFactory, IPythonSettings } from '../../client/common/types'; import { IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../client/interpreter/helpers'; import { CurrentPathService } from '../../client/interpreter/locators/services/currentPathService'; import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; import { IServiceContainer } from '../../client/ioc/types'; @@ -20,14 +21,14 @@ suite('Interpreters CurrentPath Service', () => { let fileSystem: TypeMoq.IMock; let serviceContainer: TypeMoq.IMock; let virtualEnvironmentManager: TypeMoq.IMock; - let interpreterVersionService: TypeMoq.IMock; + let interpreterHelper: TypeMoq.IMock; let pythonSettings: TypeMoq.IMock; let currentPathService: CurrentPathService; let persistentState: TypeMoq.IMock>; setup(async () => { processService = TypeMoq.Mock.ofType(); virtualEnvironmentManager = TypeMoq.Mock.ofType(); - interpreterVersionService = TypeMoq.Mock.ofType(); + interpreterHelper = TypeMoq.Mock.ofType(); const configurationService = TypeMoq.Mock.ofType(); pythonSettings = TypeMoq.Mock.ofType(); configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); @@ -43,20 +44,21 @@ suite('Interpreters CurrentPath Service', () => { serviceContainer = TypeMoq.Mock.ofType(); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IVirtualEnvironmentManager), TypeMoq.It.isAny())).returns(() => virtualEnvironmentManager.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterVersionService), TypeMoq.It.isAny())).returns(() => interpreterVersionService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterVersionService), TypeMoq.It.isAny())).returns(() => interpreterHelper.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())).returns(() => fileSystem.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPersistentStateFactory), TypeMoq.It.isAny())).returns(() => persistentStateFactory.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())).returns(() => configurationService.object); - currentPathService = new CurrentPathService(virtualEnvironmentManager.object, interpreterVersionService.object, procServiceFactory.object, serviceContainer.object); + currentPathService = new CurrentPathService(virtualEnvironmentManager.object, interpreterHelper.object, procServiceFactory.object, serviceContainer.object); }); test('Interpreters that do not exist on the file system are not excluded from the list', async () => { // Specific test for 1305 const version = 'mockVersion'; const envName = 'mockEnvName'; - interpreterVersionService.setup(v => v.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(version)); + interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version })); virtualEnvironmentManager.setup(v => v.getEnvironmentName(TypeMoq.It.isAny())).returns(() => Promise.resolve(envName)); + virtualEnvironmentManager.setup(v => v.getEnvironmentType(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(InterpreterType.VirtualEnv)); const execArgs = ['-c', 'import sys;print(sys.executable)']; pythonSettings.setup(p => p.pythonPath).returns(() => 'root:Python'); @@ -74,7 +76,7 @@ suite('Interpreters CurrentPath Service', () => { processService.verifyAll(); fileSystem.verifyAll(); expect(interpreters).to.be.of.length(2); - expect(interpreters).to.deep.include({ displayName: `${version} (${envName})`, path: 'c:/root:python', type: InterpreterType.VirtualEnv }); - expect(interpreters).to.deep.include({ displayName: `${version} (${envName})`, path: 'c:/python3', type: InterpreterType.VirtualEnv }); + expect(interpreters).to.deep.include({ version, envName, displayName: `${version} (${envName})`, path: 'c:/root:python', type: InterpreterType.VirtualEnv }); + expect(interpreters).to.deep.include({ version, envName, displayName: `${version} (${envName})`, path: 'c:/python3', type: InterpreterType.VirtualEnv }); }); }); diff --git a/src/test/interpreters/display.test.ts b/src/test/interpreters/display.test.ts index 2d9d212d41a5..b87c1ac7698f 100644 --- a/src/test/interpreters/display.test.ts +++ b/src/test/interpreters/display.test.ts @@ -4,13 +4,26 @@ import * as path from 'path'; import * as TypeMoq from 'typemoq'; 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 { Architecture, 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 { IInterpreterDisplay, IInterpreterHelper, IInterpreterService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { InterpreterDisplay } from '../../client/interpreter/display'; import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; import { IServiceContainer } from '../../client/ioc/types'; +const info: PythonInterpreter = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + type: InterpreterType.Unknown, + version: '', + version_info: [0, 0, 0, 'alpha'], + sysPrefix: '', + sysVersion: '' +}; + // tslint:disable-next-line:max-func-body-length suite('Interpreters Display', () => { let applicationShell: TypeMoq.IMock; @@ -18,7 +31,6 @@ suite('Interpreters Display', () => { 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; @@ -32,7 +44,6 @@ suite('Interpreters Display', () => { 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 = []; @@ -44,7 +55,6 @@ suite('Interpreters Display', () => { 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); @@ -72,6 +82,7 @@ suite('Interpreters Display', () => { const resource = Uri.file('x'); const workspaceFolder = Uri.file('workspace'); const activeInterpreter: PythonInterpreter = { + ...info, displayName: 'Dummy_Display_Name', type: InterpreterType.Unknown, path: path.join('user', 'development', 'env', 'bin', 'python') @@ -89,6 +100,7 @@ suite('Interpreters Display', () => { const resource = Uri.file('x'); const workspaceFolder = Uri.file('workspace'); const activeInterpreter: PythonInterpreter = { + ...info, displayName: 'Dummy_Display_Name', type: InterpreterType.Unknown, companyDisplayName: 'Company Name', @@ -114,7 +126,7 @@ suite('Interpreters Display', () => { configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('')); - versionProvider.setup(v => v.getVersion(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())).returns((_path, defaultDisplayName) => Promise.resolve(defaultDisplayName)); + interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(undefined)); await interpreterDisplay.refresh(resource); @@ -132,7 +144,7 @@ suite('Interpreters Display', () => { pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); // tslint:disable-next-line:no-any virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('Mock Name')); - versionProvider.setup(v => v.getVersion(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())).returns((_path, defaultDisplayName) => Promise.resolve(defaultDisplayName)); + interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(undefined)); await interpreterDisplay.refresh(resource); @@ -150,8 +162,7 @@ suite('Interpreters Display', () => { configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); fileSystem.setup(f => f.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(false)); - const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; - versionProvider.setup(v => v.getVersion(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())).returns(() => Promise.resolve(defaultDisplayName)); + interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(undefined)); virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('')); await interpreterDisplay.refresh(resource); @@ -171,7 +182,7 @@ suite('Interpreters Display', () => { pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); fileSystem.setup(f => f.fileExists(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)); + interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(undefined)); // tslint:disable-next-line:no-any virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('Mock Env Name')); const expectedText = `${defaultDisplayName} (Mock Env Name)`; @@ -192,7 +203,7 @@ suite('Interpreters Display', () => { pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); fileSystem.setup(f => f.fileExists(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)); + interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve({ version: displayName })); // tslint:disable-next-line:no-any virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('')); @@ -204,6 +215,7 @@ suite('Interpreters Display', () => { const workspaceFolder = Uri.file('x'); const resource = workspaceFolder; const activeInterpreter: PythonInterpreter = { + ...info, displayName: 'Dummy_Display_Name', type: InterpreterType.Unknown, companyDisplayName: 'Company Name', @@ -211,6 +223,7 @@ suite('Interpreters Display', () => { }; 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)); + interpreterHelper.setup(i => i.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); const expectedTooltip = `${activeInterpreter.path}${EOL}${activeInterpreter.companyDisplayName}`; interpreterHelper.setup(i => i.getActiveWorkspaceUri()).returns(() => { return { folderUri: workspaceFolder, configTarget: ConfigurationTarget.Workspace }; }); diff --git a/src/test/interpreters/helper.test.ts b/src/test/interpreters/helper.test.ts index ecab21922e6c..7ff97624c9d0 100644 --- a/src/test/interpreters/helper.test.ts +++ b/src/test/interpreters/helper.test.ts @@ -28,7 +28,7 @@ suite('Interpreters Display Helper', () => { }); test('getActiveWorkspaceUri should return undefined if there are no workspaces', () => { workspaceService.setup(w => w.workspaceFolders).returns(() => []); - + documentManager.setup(doc => doc.activeTextEditor).returns(() => undefined); const workspace = helper.getActiveWorkspaceUri(); expect(workspace).to.be.equal(undefined, 'incorrect value'); }); diff --git a/src/test/interpreters/interpreterService.test.ts b/src/test/interpreters/interpreterService.test.ts index 7b41dcf45a94..f6033919d9b0 100644 --- a/src/test/interpreters/interpreterService.test.ts +++ b/src/test/interpreters/interpreterService.test.ts @@ -9,7 +9,7 @@ import * as TypeMoq from 'typemoq'; import { ConfigurationTarget, Disposable, TextDocument, TextEditor, Uri, WorkspaceConfiguration } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; import { noop } from '../../client/common/core.utils'; -import { IFileSystem } from '../../client/common/platform/types'; +import { Architecture, IFileSystem } from '../../client/common/platform/types'; import { IConfigurationService, IDisposableRegistry } from '../../client/common/types'; import { IPythonPathUpdaterServiceManager } from '../../client/interpreter/configuration/types'; import { @@ -27,6 +27,19 @@ import { InterpreterService } from '../../client/interpreter/interpreterService' import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; +const info: PythonInterpreter = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + type: InterpreterType.Unknown, + version: '', + version_info: [0, 0, 0, 'alpha'], + sysPrefix: '', + sysVersion: '' +}; + // tslint:disable-next-line:max-func-body-length suite('Interpreters service', () => { let serviceManager: ServiceManager; @@ -85,6 +98,7 @@ suite('Interpreters service', () => { return { key: 'python' }; }); const interpreter: PythonInterpreter = { + ...info, path: path.join(path.sep, 'folder', 'py1', 'bin', 'python.exe'), type: InterpreterType.Unknown }; @@ -107,6 +121,7 @@ suite('Interpreters service', () => { return { key: 'python', workspaceValue: 'python' }; }); const interpreter: PythonInterpreter = { + ...info, path: 'python', type: InterpreterType.VirtualEnv }; @@ -120,6 +135,7 @@ suite('Interpreters service', () => { return { key: 'python', workspaceValue: 'elsewhere' }; }); const interpreter: PythonInterpreter = { + ...info, path: 'elsewhere', type: InterpreterType.Unknown }; @@ -135,6 +151,7 @@ suite('Interpreters service', () => { }); const intPath = path.join(path.sep, 'root', 'under', 'bin', 'python.exe'); const interpreter: PythonInterpreter = { + ...info, path: intPath, type: InterpreterType.Unknown }; diff --git a/src/test/interpreters/pipEnvService.test.ts b/src/test/interpreters/pipEnvService.test.ts index b32b6c4c74a3..39763455f3a6 100644 --- a/src/test/interpreters/pipEnvService.test.ts +++ b/src/test/interpreters/pipEnvService.test.ts @@ -15,7 +15,7 @@ import { IFileSystem } from '../../client/common/platform/types'; import { IProcessService, IProcessServiceFactory } from '../../client/common/process/types'; import { ICurrentProcess, IPersistentState, IPersistentStateFactory } from '../../client/common/types'; import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; -import { IInterpreterLocatorService, IInterpreterVersionService } from '../../client/interpreter/contracts'; +import { IInterpreterHelper, IInterpreterLocatorService } from '../../client/interpreter/contracts'; import { PipEnvService } from '../../client/interpreter/locators/services/pipEnvService'; import { IServiceContainer } from '../../client/ioc/types'; @@ -31,7 +31,7 @@ suite('Interpreters - PipEnv', () => { let pipEnvService: IInterpreterLocatorService; let serviceContainer: TypeMoq.IMock; - let interpreterVersionService: TypeMoq.IMock; + let interpreterHelper: TypeMoq.IMock; let processService: TypeMoq.IMock; let currentProcess: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; @@ -42,7 +42,7 @@ suite('Interpreters - PipEnv', () => { setup(() => { serviceContainer = TypeMoq.Mock.ofType(); const workspaceService = TypeMoq.Mock.ofType(); - interpreterVersionService = TypeMoq.Mock.ofType(); + interpreterHelper = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); processService = TypeMoq.Mock.ofType(); appShell = TypeMoq.Mock.ofType(); @@ -67,7 +67,7 @@ suite('Interpreters - PipEnv', () => { serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProcessServiceFactory), TypeMoq.It.isAny())).returns(() => procServiceFactory.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterVersionService))).returns(() => interpreterVersionService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterHelper))).returns(() => interpreterHelper.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICurrentProcess))).returns(() => currentProcess.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); @@ -116,12 +116,13 @@ suite('Interpreters - PipEnv', () => { }); test(`Should return interpreter information${testSuffix}`, async () => { const env = {}; - const venvDir = 'one'; + const pythonPath = 'one'; + envVarsProvider.setup(e => e.getEnvironmentVariables(TypeMoq.It.isAny())).returns(() => Promise.resolve({})).verifiable(TypeMoq.Times.once()); currentProcess.setup(c => c.env).returns(() => env); - processService.setup(p => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: venvDir })); - interpreterVersionService.setup(v => v.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('xyz')); + processService.setup(p => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: pythonPath })); + interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: 'xyz' })); fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, 'Pipfile')))).returns(() => Promise.resolve(true)).verifiable(); - fileSystem.setup(fs => fs.directoryExists(TypeMoq.It.isValue(venvDir))).returns(() => Promise.resolve(true)).verifiable(); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)).verifiable(); const environments = await pipEnvService.getInterpreters(resource); expect(environments).to.be.lengthOf(1); @@ -132,13 +133,14 @@ suite('Interpreters - PipEnv', () => { const env = { PIPENV_PIPFILE: envPipFile }; - const venvDir = 'one'; + const pythonPath = 'one'; + envVarsProvider.setup(e => e.getEnvironmentVariables(TypeMoq.It.isAny())).returns(() => Promise.resolve({})).verifiable(TypeMoq.Times.once()); currentProcess.setup(c => c.env).returns(() => env); - processService.setup(p => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: venvDir })); - interpreterVersionService.setup(v => v.getVersion(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('xyz')); + processService.setup(p => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: pythonPath })); + interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version: 'xyz' })); fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, 'Pipfile')))).returns(() => Promise.resolve(false)).verifiable(TypeMoq.Times.never()); fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, envPipFile)))).returns(() => Promise.resolve(true)).verifiable(TypeMoq.Times.once()); - fileSystem.setup(fs => fs.directoryExists(TypeMoq.It.isValue(venvDir))).returns(() => Promise.resolve(true)).verifiable(); + fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)).verifiable(); const environments = await pipEnvService.getInterpreters(resource); expect(environments).to.be.lengthOf(1); diff --git a/src/test/interpreters/virtualEnvManager.test.ts b/src/test/interpreters/virtualEnvManager.test.ts index 4db50c4ac344..de16e18c03bd 100644 --- a/src/test/interpreters/virtualEnvManager.test.ts +++ b/src/test/interpreters/virtualEnvManager.test.ts @@ -6,9 +6,14 @@ import { expect } from 'chai'; import { Container } from 'inversify'; import * as TypeMoq from 'typemoq'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { FileSystem } from '../../client/common/platform/fileSystem'; +import { PlatformService } from '../../client/common/platform/platformService'; +import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; import { BufferDecoder } from '../../client/common/process/decoder'; import { ProcessService } from '../../client/common/process/proc'; import { IBufferDecoder, IProcessService, IProcessServiceFactory } from '../../client/common/process/types'; +import { IPipEnvService } from '../../client/interpreter/contracts'; import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; @@ -33,6 +38,10 @@ suite('Virtual environment manager', () => { processServiceFactory.setup(f => f.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(new ProcessService(new BufferDecoder(), process.env as any))); serviceManager.addSingletonInstance(IProcessServiceFactory, processServiceFactory.object); serviceManager.addSingleton(IBufferDecoder, BufferDecoder); + serviceManager.addSingleton(IFileSystem, FileSystem); + serviceManager.addSingleton(IPlatformService, PlatformService); + serviceManager.addSingletonInstance(IPipEnvService, TypeMoq.Mock.ofType().object); + serviceManager.addSingletonInstance(IWorkspaceService, TypeMoq.Mock.ofType().object); const venvManager = new VirtualEnvironmentManager(serviceContainer); const name = await venvManager.getEnvironmentName(PYTHON_PATH); const result = name === '' || name === 'venv' || name === 'virtualenv'; @@ -45,6 +54,9 @@ suite('Virtual environment manager', () => { processService.setup((x: any) => x.then).returns(() => undefined); processServiceFactory.setup(f => f.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); serviceManager.addSingletonInstance(IProcessServiceFactory, processServiceFactory.object); + serviceManager.addSingletonInstance(IFileSystem, TypeMoq.Mock.ofType().object); + serviceManager.addSingletonInstance(IPipEnvService, TypeMoq.Mock.ofType().object); + serviceManager.addSingletonInstance(IWorkspaceService, TypeMoq.Mock.ofType().object); const venvManager = new VirtualEnvironmentManager(serviceContainer); processService diff --git a/src/test/interpreters/windowsRegistryService.test.ts b/src/test/interpreters/windowsRegistryService.test.ts index eba551ea4b50..c06563cd0c8d 100644 --- a/src/test/interpreters/windowsRegistryService.test.ts +++ b/src/test/interpreters/windowsRegistryService.test.ts @@ -4,6 +4,7 @@ import * as TypeMoq from 'typemoq'; import { Architecture, RegistryHive } from '../../client/common/platform/types'; import { IPersistentStateFactory } from '../../client/common/types'; import { IS_WINDOWS } from '../../client/debugger/Common/Utils'; +import { IInterpreterHelper } from '../../client/interpreter/contracts'; import { WindowsRegistryService } from '../../client/interpreter/locators/services/windowsRegistryService'; import { IServiceContainer } from '../../client/ioc/types'; import { initialize, initializeTest } from '../initialize'; @@ -18,8 +19,12 @@ suite('Interpreters from Windows Registry', () => { setup(() => { serviceContainer = TypeMoq.Mock.ofType(); const stateFactory = TypeMoq.Mock.ofType(); + const interpreterHelper = TypeMoq.Mock.ofType(); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPersistentStateFactory))).returns(() => stateFactory.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterHelper))).returns(() => interpreterHelper.object); const state = new MockState(undefined); + // tslint:disable-next-line:no-empty no-any + interpreterHelper.setup(h => h.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({} as any)); stateFactory.setup(s => s.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => state); return initializeTest(); }); @@ -182,7 +187,7 @@ suite('Interpreters from Windows Registry', () => { { key: '\\Software\\Python', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['A'] }, { key: '\\Software\\Python\\Company A', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['Another Tag'] } ]; - const registryValues: { key: string, hive: RegistryHive, arch?: Architecture, value: string, name?: string }[] = [ + const registryValues: { key: string; hive: RegistryHive; arch?: Architecture; value: string; name?: string }[] = [ { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), name: 'ExecutablePath' }, @@ -241,7 +246,7 @@ suite('Interpreters from Windows Registry', () => { { key: '\\Software\\Python', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['A'] }, { key: '\\Software\\Python\\Company A', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['Another Tag'] } ]; - const registryValues: { key: string, hive: RegistryHive, arch?: Architecture, value: string, name?: string }[] = [ + const registryValues: { key: string; hive: RegistryHive; arch?: Architecture; value: string; name?: string }[] = [ { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), name: 'ExecutablePath' }, diff --git a/src/test/unittests/serviceRegistry.ts b/src/test/unittests/serviceRegistry.ts index f3a6d21d2315..88b73407a1d1 100644 --- a/src/test/unittests/serviceRegistry.ts +++ b/src/test/unittests/serviceRegistry.ts @@ -32,7 +32,7 @@ export class UnitTestIocContainer extends IocContainer { super(); } public getPythonMajorVersion(resource: Uri) { - return this.serviceContainer.get(IPythonExecutionFactory).create(resource) + return this.serviceContainer.get(IPythonExecutionFactory).create({ resource }) .then(pythonProcess => pythonProcess.exec(['-c', 'import sys;print(sys.version_info[0])'], {})) .then(output => parseInt(output.stdout.trim(), 10)); }