From 402f1fa40bb0183c66bb23cb0eea69c9e7e03369 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 22 Nov 2021 10:28:15 -0800 Subject: [PATCH] Add API to consume new Python API (but no used) (#8324) --- .eslintrc.js | 1 - src/client/api/types.ts | 11 +++ src/client/common/process/condaService.ts | 89 +++++++++++++++++++ .../common/process/pythonExecutionFactory.ts | 2 +- src/client/common/process/serviceRegistry.ts | 2 + .../process/pythonEnvironment.unit.test.ts | 8 +- 6 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 src/client/common/process/condaService.ts diff --git a/.eslintrc.js b/.eslintrc.js index c0a25eb486f..0da35aa80f7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -565,7 +565,6 @@ module.exports = { 'src/client/common/installer/productService.ts', 'src/client/common/installer/pipInstaller.ts', 'src/client/common/process/currentProcess.ts', - 'src/client/common/process/serviceRegistry.ts', 'src/client/common/process/pythonToolService.ts', 'src/client/common/process/internal/scripts/testing_tools.ts', 'src/client/common/process/pythonDaemonPool.ts', diff --git a/src/client/api/types.ts b/src/client/api/types.ts index 5c9a5806c59..07d064bc649 100644 --- a/src/client/api/types.ts +++ b/src/client/api/types.ts @@ -7,6 +7,8 @@ import { InterpreterUri } from '../common/installer/types'; import { InstallerResponse, Product, Resource } from '../common/types'; import { IInterpreterQuickPickItem } from '../interpreter/configuration/types'; import { PythonEnvironment } from '../pythonEnvironments/info'; +import type { SemVer } from 'semver'; + export type ILanguageServerConnection = Pick< lsp.ProtocolConnection, 'sendRequest' | 'sendNotification' | 'onProgress' | 'sendProgress' | 'onNotification' | 'onRequest' @@ -111,6 +113,15 @@ export type PythonApi = { * Registers a visibility filter for the interpreter status bar. */ registerInterpreterStatusFilter(filter: IInterpreterStatusbarVisibilityFilter): void; + getCondaVersion?(): Promise; + /** + * Returns the conda executable. + */ + getCondaFile?(): Promise; + getEnvironmentActivationShellCommands?( + resource: Resource, + interpreter?: PythonEnvironment + ): Promise; }; export const IPythonInstaller = Symbol('IPythonInstaller'); diff --git a/src/client/common/process/condaService.ts b/src/client/common/process/condaService.ts new file mode 100644 index 00000000000..aaf0d58140f --- /dev/null +++ b/src/client/common/process/condaService.ts @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable, named } from 'inversify'; +import { SemVer } from 'semver'; +import { Memento } from 'vscode'; +import { IPythonApiProvider } from '../../api/types'; +import { IFileSystem } from '../platform/types'; +import { GLOBAL_MEMENTO, IMemento } from '../types'; +import { createDeferredFromPromise } from '../utils/async'; + +const CACHEKEY_FOR_CONDA_INFO = 'CONDA_INFORMATION_CACHE'; + +@injectable() +export class CondaService { + private _file?: string; + private _version?: SemVer; + constructor( + @inject(IPythonApiProvider) private readonly pythonApi: IPythonApiProvider, + @inject(IMemento) @named(GLOBAL_MEMENTO) private readonly globalState: Memento, + @inject(IFileSystem) private readonly fs: IFileSystem + ) {} + async getCondaVersion() { + if (this._version) { + return this._version; + } + const latestInfo = this.pythonApi + .getApi() + .then((api) => (api.getCondaVersion ? api.getCondaVersion() : undefined)); + void latestInfo.then((version) => { + this._version = version; + void this.udpateCache(); + }); + const cachedInfo = createDeferredFromPromise(this.getCachedInformation()); + await Promise.race([cachedInfo, latestInfo]); + if (cachedInfo.completed && cachedInfo.value?.version) { + return (this._version = cachedInfo.value.version); + } + return latestInfo; + } + async getCondaFile() { + if (this._file) { + return this._file; + } + const latestInfo = this.pythonApi.getApi().then((api) => (api.getCondaFile ? api.getCondaFile() : undefined)); + void latestInfo.then((file) => { + this._file = file; + void this.udpateCache(); + }); + const cachedInfo = createDeferredFromPromise(this.getCachedInformation()); + await Promise.race([cachedInfo, latestInfo]); + if (cachedInfo.completed && cachedInfo.value?.file) { + return (this._file = cachedInfo.value.file); + } + return latestInfo; + } + private async udpateCache() { + if (!this._file || !this._version) { + return; + } + const fileHash = await this.fs.getFileHash(this._file); + await this.globalState.update(CACHEKEY_FOR_CONDA_INFO, { + version: this._version.raw, + file: this._file, + fileHash + }); + } + /** + * If the last modified date of the conda file is the same as when we last checked, + * then we can assume the version is the same. + * Even if not, we'll update this with the latest information. + */ + private async getCachedInformation(): Promise<{ version: SemVer; file: string } | undefined> { + const cachedInfo = this.globalState.get<{ version: string; file: string; fileHash: string } | undefined>( + CACHEKEY_FOR_CONDA_INFO, + undefined + ); + if (!cachedInfo) { + return; + } + const fileHash = await this.fs.getFileHash(cachedInfo.file); + if (cachedInfo.fileHash === fileHash) { + return { + version: new SemVer(cachedInfo.version), + file: cachedInfo.file + }; + } + } +} diff --git a/src/client/common/process/pythonExecutionFactory.ts b/src/client/common/process/pythonExecutionFactory.ts index a2cd3e2fed2..5cd7f487c46 100644 --- a/src/client/common/process/pythonExecutionFactory.ts +++ b/src/client/common/process/pythonExecutionFactory.ts @@ -71,7 +71,7 @@ export class PythonExecutionFactory implements IPythonExecutionFactory { ); } - @traceDecorators.verbose('Create daemon') + @traceDecorators.verbose('Create daemon', TraceOptions.BeforeCall | TraceOptions.Arguments) public async createDaemon( options: DaemonExecutionFactoryCreationOptions ): Promise { diff --git a/src/client/common/process/serviceRegistry.ts b/src/client/common/process/serviceRegistry.ts index 71d0eb7a900..31baf48c78d 100644 --- a/src/client/common/process/serviceRegistry.ts +++ b/src/client/common/process/serviceRegistry.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { IServiceManager } from '../../ioc/types'; +import { CondaService } from './condaService'; import { BufferDecoder } from './decoder'; import { ProcessServiceFactory } from './processFactory'; import { PythonExecutionFactory } from './pythonExecutionFactory'; @@ -11,4 +12,5 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IBufferDecoder, BufferDecoder); serviceManager.addSingleton(IProcessServiceFactory, ProcessServiceFactory); serviceManager.addSingleton(IPythonExecutionFactory, PythonExecutionFactory); + serviceManager.addSingleton(CondaService, CondaService); } diff --git a/src/test/common/process/pythonEnvironment.unit.test.ts b/src/test/common/process/pythonEnvironment.unit.test.ts index 6cdbf370a0b..9d63d3ced4d 100644 --- a/src/test/common/process/pythonEnvironment.unit.test.ts +++ b/src/test/common/process/pythonEnvironment.unit.test.ts @@ -231,7 +231,7 @@ suite('CondaEnvironment', () => { }); test('getExecutionInfo with a named environment should return execution info using the environment name', () => { - const condaInfo = { name: 'foo', path: 'bar' }; + const condaInfo = { name: 'foo', path: 'bar', version: undefined }; const env = createCondaEnv(condaFile, condaInfo, pythonPath, processService.object, fileSystem.object); const result = env.getExecutionInfo(args); @@ -245,7 +245,7 @@ suite('CondaEnvironment', () => { }); test('getExecutionInfo with a non-named environment should return execution info using the environment path', () => { - const condaInfo = { name: '', path: 'bar' }; + const condaInfo = { name: '', path: 'bar', version: undefined }; const env = createCondaEnv(condaFile, condaInfo, pythonPath, processService.object, fileSystem.object); const result = env.getExecutionInfo(args); @@ -260,7 +260,7 @@ suite('CondaEnvironment', () => { test('getExecutionObservableInfo with a named environment should return execution info using pythonPath only', () => { const expected = { command: pythonPath, args, python: [pythonPath], pythonExecutable: pythonPath }; - const condaInfo = { name: 'foo', path: 'bar' }; + const condaInfo = { name: 'foo', path: 'bar', version: undefined }; const env = createCondaEnv(condaFile, condaInfo, pythonPath, processService.object, fileSystem.object); const result = env.getExecutionObservableInfo(args); @@ -270,7 +270,7 @@ suite('CondaEnvironment', () => { test('getExecutionObservableInfo with a non-named environment should return execution info using pythonPath only', () => { const expected = { command: pythonPath, args, python: [pythonPath], pythonExecutable: pythonPath }; - const condaInfo = { name: '', path: 'bar' }; + const condaInfo = { name: '', path: 'bar', version: undefined }; const env = createCondaEnv(condaFile, condaInfo, pythonPath, processService.object, fileSystem.object); const result = env.getExecutionObservableInfo(args);