Skip to content

Create a new API to retrieve interpreter details with the ability to cache the details #1567

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/3 Code Health/1569.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Create a new API to retrieve interpreter details with the ability to cache the details.
13 changes: 13 additions & 0 deletions pythonFiles/interpreterInfo.py
Original file line number Diff line number Diff line change
@@ -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))
2 changes: 1 addition & 1 deletion src/client/activation/interpreterDataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class InterpreterDataService {

public async getInterpreterData(resource?: Uri): Promise<InterpreterData | undefined> {
const executionFactory = this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory);
const execService = await executionFactory.create(resource);
const execService = await executionFactory.create({ resource });

const interpreterPath = await execService.getExecutablePath();
if (interpreterPath.length === 0) {
Expand Down
6 changes: 3 additions & 3 deletions src/client/common/installer/channelManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<typeof options[0]>(options, { matchOnDescription: true, matchOnDetail: true, placeHolder });
return selection ? selection.installer : undefined;
}

Expand Down
2 changes: 1 addition & 1 deletion src/client/common/installer/pipInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class PipInstaller extends ModuleInstaller implements IModuleInstaller {
}
private isPipAvailable(resource?: Uri): Promise<boolean> {
const pythonExecutionFactory = this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory);
return pythonExecutionFactory.create(resource)
return pythonExecutionFactory.create({ resource })
.then(proc => proc.isModuleInstalled('pip'))
.catch(() => false);
}
Expand Down
2 changes: 1 addition & 1 deletion src/client/common/installer/productInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>(IPythonExecutionFactory).create(resource);
const pythonProcess = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create({ resource });
return pythonProcess.isModuleInstalled(executableName);
} else {
const process = await this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create(resource);
Expand Down
29 changes: 21 additions & 8 deletions src/client/common/persistentState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,38 @@ import { Memento } from 'vscode';
import { GLOBAL_MEMENTO, IMemento, IPersistentState, IPersistentStateFactory, WORKSPACE_MEMENTO } from './types';

class PersistentState<T> implements IPersistentState<T>{
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<T>(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<T>(this.key, this.defaultValue!);
}
}

public async updateValue(newValue: T): Promise<void> {
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<T>(key: string, defaultValue: T): IPersistentState<T> {
return new PersistentState<T>(this.globalState, key, defaultValue);
public createGlobalPersistentState<T>(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState<T> {
return new PersistentState<T>(this.globalState, key, defaultValue, expiryDurationMs);
}
public createWorkspacePersistentState<T>(key: string, defaultValue: T): IPersistentState<T> {
return new PersistentState<T>(this.workspaceState, key, defaultValue);
public createWorkspacePersistentState<T>(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState<T> {
return new PersistentState<T>(this.workspaceState, key, defaultValue, expiryDurationMs);
}
}
13 changes: 13 additions & 0 deletions src/client/common/platform/fileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<string | undefined> {
return new Promise<string | undefined>(resolve => {
fs.lstat(filePath, (err, stats) => {
if (err) {
resolve();
} else {
const actual = createHash('sha512').update(`${stats.ctimeMs}-${stats.mtimeMs}`).digest('hex');
resolve(actual);
}
});
});
}
}
1 change: 1 addition & 0 deletions src/client/common/platform/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ export interface IFileSystem {
getRealPath(path: string): Promise<string>;
copyFile(src: string, dest: string): Promise<void>;
deleteFile(filename: string): Promise<void>;
getFileHash(filePath: string): Promise<string | undefined>;
}
12 changes: 8 additions & 4 deletions src/client/common/process/pythonExecutionFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>(IProcessServiceFactory);
this.configService = serviceContainer.get<IConfigurationService>(IConfigurationService);
}
public async create(resource?: Uri): Promise<IPythonExecutionService> {
const processService = await this.processServiceFactory.create(resource);
return new PythonExecutionService(this.serviceContainer, processService, resource);
public async create(options: ExecutionFactoryCreationOptions): Promise<IPythonExecutionService> {
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);
}
}
41 changes: 28 additions & 13 deletions src/client/common/process/pythonProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>(IConfigurationService);
constructor(private serviceContainer: IServiceContainer, private readonly procService: IProcessService, private readonly pythonPath: string) {
this.fileSystem = serviceContainer.get<IFileSystem>(IFileSystem);
}

public async getVersion(): Promise<string> {
const versionService = this.serviceContainer.get<IInterpreterVersionService>(IInterpreterVersionService);
return versionService.getVersion(this.pythonPath, '');
public async getInterpreterInformation(): Promise<InterpreterInfomation | undefined> {
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<string> {
// If we've passed the python file, then return the file.
Expand Down Expand Up @@ -65,7 +83,4 @@ export class PythonExecutionService implements IPythonExecutionService {

return result;
}
private get pythonPath(): string {
return this.configService.getSettings(this.resource).pythonPath;
}
}
4 changes: 2 additions & 2 deletions src/client/common/process/pythonToolService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>(IPythonExecutionFactory).create(resource);
const pythonExecutionService = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create({ resource });
return pythonExecutionService.execModuleObservable(executionInfo.moduleName, executionInfo.args, options);
} else {
const processService = await this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create(resource);
Expand All @@ -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>(IPythonExecutionFactory).create(resource);
const pythonExecutionService = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create({ resource });
return pythonExecutionService.execModule(executionInfo.moduleName!, executionInfo.args, options);
} else {
const processService = await this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create(resource);
Expand Down
21 changes: 18 additions & 3 deletions src/client/common/process/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<IPythonExecutionService>;
create(options: ExecutionFactoryCreationOptions): Promise<IPythonExecutionService>;
}

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<InterpreterInfomation | undefined>;
getExecutablePath(): Promise<string>;
isModuleInstalled(moduleName: string): Promise<boolean>;

Expand Down
4 changes: 2 additions & 2 deletions src/client/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export interface IPersistentState<T> {
export const IPersistentStateFactory = Symbol('IPersistentStateFactory');

export interface IPersistentStateFactory {
createGlobalPersistentState<T>(key: string, defaultValue: T): IPersistentState<T>;
createWorkspacePersistentState<T>(key: string, defaultValue: T): IPersistentState<T>;
createGlobalPersistentState<T>(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState<T>;
createWorkspacePersistentState<T>(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState<T>;
}

export type ExecutionInfo = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class ConfigurationProviderUtils implements IConfigurationProviderUtils {
}
public async getPyramidStartupScriptFilePath(resource?: Uri): Promise<string | undefined> {
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;
Expand Down
19 changes: 11 additions & 8 deletions src/client/interpreter/configuration/pythonPathUpdaterService.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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>(IPythonPathUpdaterServiceFactory);
this.interpreterVersionService = serviceContainer.get<IInterpreterVersionService>(IInterpreterVersionService);
this.executionFactory = serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory);
}
public async updatePythonPath(pythonPath: string, configTarget: ConfigurationTarget, trigger: 'ui' | 'shebang' | 'load', wkspace?: Uri): Promise<void> {
const stopWatch = new StopWatch();
Expand All @@ -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<InterpreterInfomation>(() => 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<string>(() => 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);
Expand Down
Loading