Skip to content

Commit a4374fe

Browse files
authored
Create a new API to retrieve interpreter details with the ability to cache the details (microsoft#1567)
Fixes microsoft#1569
1 parent f18a5ee commit a4374fe

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+631
-256
lines changed

news/3 Code Health/1569.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Create a new API to retrieve interpreter details with the ability to cache the details.

pythonFiles/interpreterInfo.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import json
5+
import sys
6+
7+
obj = {}
8+
obj["versionInfo"] = sys.version_info[:4]
9+
obj["sysPrefix"] = sys.prefix
10+
obj["version"] = sys.version
11+
obj["is64Bit"] = sys.maxsize > 2**32
12+
13+
print(json.dumps(obj))

src/client/activation/interpreterDataService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class InterpreterDataService {
3333

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

3838
const interpreterPath = await execService.getExecutablePath();
3939
if (interpreterPath.length === 0) {

src/client/common/installer/channelManager.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT License.
33

44
import { inject, injectable } from 'inversify';
5-
import { QuickPickItem, Uri } from 'vscode';
5+
import { Uri } from 'vscode';
66
import { IInterpreterService, InterpreterType } from '../../interpreter/contracts';
77
import { IServiceContainer } from '../../ioc/types';
88
import { IApplicationShell } from '../application/types';
@@ -34,9 +34,9 @@ export class InstallationChannelManager implements IInstallationChannelManager {
3434
label: `Install using ${installer.displayName}`,
3535
description: '',
3636
installer
37-
} as QuickPickItem & { installer: IModuleInstaller };
37+
};
3838
});
39-
const selection = await appShell.showQuickPick(options, { matchOnDescription: true, matchOnDetail: true, placeHolder });
39+
const selection = await appShell.showQuickPick<typeof options[0]>(options, { matchOnDescription: true, matchOnDetail: true, placeHolder });
4040
return selection ? selection.installer : undefined;
4141
}
4242

src/client/common/installer/pipInstaller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export class PipInstaller extends ModuleInstaller implements IModuleInstaller {
3838
}
3939
private isPipAvailable(resource?: Uri): Promise<boolean> {
4040
const pythonExecutionFactory = this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory);
41-
return pythonExecutionFactory.create(resource)
41+
return pythonExecutionFactory.create({ resource })
4242
.then(proc => proc.isModuleInstalled('pip'))
4343
.catch(() => false);
4444
}

src/client/common/installer/productInstaller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ abstract class BaseInstaller {
7474

7575
const isModule = typeof moduleName === 'string' && moduleName.length > 0 && path.basename(executableName) === executableName;
7676
if (isModule) {
77-
const pythonProcess = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create(resource);
77+
const pythonProcess = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create({ resource });
7878
return pythonProcess.isModuleInstalled(executableName);
7979
} else {
8080
const process = await this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create(resource);

src/client/common/persistentState.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,38 @@ import { Memento } from 'vscode';
88
import { GLOBAL_MEMENTO, IMemento, IPersistentState, IPersistentStateFactory, WORKSPACE_MEMENTO } from './types';
99

1010
class PersistentState<T> implements IPersistentState<T>{
11-
constructor(private storage: Memento, private key: string, private defaultValue: T) { }
11+
constructor(private storage: Memento, private key: string, private defaultValue?: T, private expiryDurationMs?: number) { }
1212

1313
public get value(): T {
14-
return this.storage.get<T>(this.key, this.defaultValue);
14+
if (this.expiryDurationMs) {
15+
const cachedData = this.storage.get<{ data?: T; expiry?: number }>(this.key, { data: this.defaultValue! });
16+
if (!cachedData || !cachedData.expiry || cachedData.expiry < Date.now()) {
17+
return this.defaultValue!;
18+
} else {
19+
return cachedData.data!;
20+
}
21+
} else {
22+
return this.storage.get<T>(this.key, this.defaultValue!);
23+
}
1524
}
1625

1726
public async updateValue(newValue: T): Promise<void> {
18-
await this.storage.update(this.key, newValue);
27+
if (this.expiryDurationMs) {
28+
await this.storage.update(this.key, { data: newValue, expiry: Date.now() + this.expiryDurationMs });
29+
} else {
30+
await this.storage.update(this.key, newValue);
31+
}
1932
}
2033
}
2134

2235
@injectable()
2336
export class PersistentStateFactory implements IPersistentStateFactory {
24-
constructor( @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento,
37+
constructor(@inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento,
2538
@inject(IMemento) @named(WORKSPACE_MEMENTO) private workspaceState: Memento) { }
26-
public createGlobalPersistentState<T>(key: string, defaultValue: T): IPersistentState<T> {
27-
return new PersistentState<T>(this.globalState, key, defaultValue);
39+
public createGlobalPersistentState<T>(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState<T> {
40+
return new PersistentState<T>(this.globalState, key, defaultValue, expiryDurationMs);
2841
}
29-
public createWorkspacePersistentState<T>(key: string, defaultValue: T): IPersistentState<T> {
30-
return new PersistentState<T>(this.workspaceState, key, defaultValue);
42+
public createWorkspacePersistentState<T>(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState<T> {
43+
return new PersistentState<T>(this.workspaceState, key, defaultValue, expiryDurationMs);
3144
}
3245
}

src/client/common/platform/fileSystem.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33
'use strict';
44

5+
import { createHash } from 'crypto';
56
import * as fs from 'fs-extra';
67
import { inject, injectable } from 'inversify';
78
import * as path from 'path';
@@ -117,4 +118,16 @@ export class FileSystem implements IFileSystem {
117118
fs.unlink(filename, err => err ? deferred.reject(err) : deferred.resolve());
118119
return deferred.promise;
119120
}
121+
public getFileHash(filePath: string): Promise<string | undefined> {
122+
return new Promise<string | undefined>(resolve => {
123+
fs.lstat(filePath, (err, stats) => {
124+
if (err) {
125+
resolve();
126+
} else {
127+
const actual = createHash('sha512').update(`${stats.ctimeMs}-${stats.mtimeMs}`).digest('hex');
128+
resolve(actual);
129+
}
130+
});
131+
});
132+
}
120133
}

src/client/common/platform/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,5 @@ export interface IFileSystem {
4646
getRealPath(path: string): Promise<string>;
4747
copyFile(src: string, dest: string): Promise<void>;
4848
deleteFile(filename: string): Promise<void>;
49+
getFileHash(filePath: string): Promise<string | undefined>;
4950
}

src/client/common/process/pythonExecutionFactory.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@
44
import { inject, injectable } from 'inversify';
55
import { Uri } from 'vscode';
66
import { IServiceContainer } from '../../ioc/types';
7+
import { IConfigurationService } from '../types';
78
import { PythonExecutionService } from './pythonProcess';
8-
import { IProcessServiceFactory, IPythonExecutionFactory, IPythonExecutionService } from './types';
9+
import { ExecutionFactoryCreationOptions, IProcessServiceFactory, IPythonExecutionFactory, IPythonExecutionService } from './types';
910

1011
@injectable()
1112
export class PythonExecutionFactory implements IPythonExecutionFactory {
13+
private readonly configService: IConfigurationService;
1214
private processServiceFactory: IProcessServiceFactory;
1315
constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {
1416
this.processServiceFactory = serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory);
17+
this.configService = serviceContainer.get<IConfigurationService>(IConfigurationService);
1518
}
16-
public async create(resource?: Uri): Promise<IPythonExecutionService> {
17-
const processService = await this.processServiceFactory.create(resource);
18-
return new PythonExecutionService(this.serviceContainer, processService, resource);
19+
public async create(options: ExecutionFactoryCreationOptions): Promise<IPythonExecutionService> {
20+
const pythonPath = options.pythonPath ? options.pythonPath : this.configService.getSettings(options.resource).pythonPath;
21+
const processService = await this.processServiceFactory.create(options.resource);
22+
return new PythonExecutionService(this.serviceContainer, processService, pythonPath);
1923
}
2024
}

src/client/common/process/pythonProcess.ts

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,46 @@
22
// Licensed under the MIT License.
33

44
import { injectable } from 'inversify';
5+
import * as path from 'path';
56
import { Uri } from 'vscode';
6-
import { IInterpreterVersionService } from '../../interpreter/contracts';
77
import { IServiceContainer } from '../../ioc/types';
8+
import { EXTENSION_ROOT_DIR } from '../constants';
89
import { ErrorUtils } from '../errors/errorUtils';
910
import { ModuleNotInstalledError } from '../errors/moduleNotInstalledError';
10-
import { IFileSystem } from '../platform/types';
11-
import { IConfigurationService } from '../types';
12-
import { ExecutionResult, IProcessService, IPythonExecutionService, ObservableExecutionResult, SpawnOptions } from './types';
11+
import { Architecture, IFileSystem } from '../platform/types';
12+
import { EnvironmentVariables } from '../variables/types';
13+
import { ExecutionResult, InterpreterInfomation, IProcessService, IPythonExecutionService, ObservableExecutionResult, PythonVersionInfo, SpawnOptions } from './types';
1314

1415
@injectable()
1516
export class PythonExecutionService implements IPythonExecutionService {
16-
private readonly configService: IConfigurationService;
1717
private readonly fileSystem: IFileSystem;
1818

19-
constructor(private serviceContainer: IServiceContainer, private readonly procService: IProcessService, private resource?: Uri) {
20-
this.configService = serviceContainer.get<IConfigurationService>(IConfigurationService);
19+
constructor(private serviceContainer: IServiceContainer, private readonly procService: IProcessService, private readonly pythonPath: string) {
2120
this.fileSystem = serviceContainer.get<IFileSystem>(IFileSystem);
2221
}
2322

24-
public async getVersion(): Promise<string> {
25-
const versionService = this.serviceContainer.get<IInterpreterVersionService>(IInterpreterVersionService);
26-
return versionService.getVersion(this.pythonPath, '');
23+
public async getInterpreterInformation(): Promise<InterpreterInfomation | undefined> {
24+
const file = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'interpreterInfo.py');
25+
try {
26+
const [version, jsonValue] = await Promise.all([
27+
this.procService.exec(this.pythonPath, ['--version'], { mergeStdOutErr: true })
28+
.then(output => output.stdout.trim()),
29+
this.procService.exec(this.pythonPath, [file], { mergeStdOutErr: true })
30+
.then(output => output.stdout.trim())
31+
]);
32+
33+
const json = JSON.parse(jsonValue) as { versionInfo: PythonVersionInfo; sysPrefix: string; sysVersion: string; is64Bit: boolean };
34+
return {
35+
architecture: json.is64Bit ? Architecture.x64 : Architecture.x86,
36+
path: this.pythonPath,
37+
version,
38+
sysVersion: json.sysVersion,
39+
version_info: json.versionInfo,
40+
sysPrefix: json.sysPrefix
41+
};
42+
} catch (ex) {
43+
console.error(`Failed to get interpreter information for '${this.pythonPath}'`, ex);
44+
}
2745
}
2846
public async getExecutablePath(): Promise<string> {
2947
// If we've passed the python file, then return the file.
@@ -65,7 +83,4 @@ export class PythonExecutionService implements IPythonExecutionService {
6583

6684
return result;
6785
}
68-
private get pythonPath(): string {
69-
return this.configService.getSettings(this.resource).pythonPath;
70-
}
7186
}

src/client/common/process/pythonToolService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class PythonToolExecutionService implements IPythonToolExecutionService {
1515
throw new Error('Environment variables are not supported');
1616
}
1717
if (executionInfo.moduleName && executionInfo.moduleName.length > 0) {
18-
const pythonExecutionService = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create(resource);
18+
const pythonExecutionService = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create({ resource });
1919
return pythonExecutionService.execModuleObservable(executionInfo.moduleName, executionInfo.args, options);
2020
} else {
2121
const processService = await this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create(resource);
@@ -27,7 +27,7 @@ export class PythonToolExecutionService implements IPythonToolExecutionService {
2727
throw new Error('Environment variables are not supported');
2828
}
2929
if (executionInfo.moduleName && executionInfo.moduleName.length > 0) {
30-
const pythonExecutionService = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create(resource);
30+
const pythonExecutionService = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create({ resource });
3131
return pythonExecutionService.execModule(executionInfo.moduleName!, executionInfo.args, options);
3232
} else {
3333
const processService = await this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create(resource);

src/client/common/process/types.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { ChildProcess, SpawnOptions as ChildProcessSpawnOptions } from 'child_process';
55
import { Observable } from 'rxjs/Observable';
66
import { CancellationToken, Uri } from 'vscode';
7+
import { Architecture } from '../platform/types';
78
import { ExecutionInfo } from '../types';
89
import { EnvironmentVariables } from '../variables/types';
910

@@ -46,14 +47,28 @@ export interface IProcessServiceFactory {
4647
}
4748

4849
export const IPythonExecutionFactory = Symbol('IPythonExecutionFactory');
49-
50+
export type ExecutionFactoryCreationOptions = {
51+
resource?: Uri;
52+
pythonPath?: string;
53+
};
5054
export interface IPythonExecutionFactory {
51-
create(resource?: Uri): Promise<IPythonExecutionService>;
55+
create(options: ExecutionFactoryCreationOptions): Promise<IPythonExecutionService>;
5256
}
53-
57+
export type ReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final';
58+
// tslint:disable-next-line:interface-name
59+
export type PythonVersionInfo = [number, number, number, ReleaseLevel];
60+
export type InterpreterInfomation = {
61+
path: string;
62+
version: string;
63+
sysVersion: string;
64+
architecture: Architecture;
65+
version_info: PythonVersionInfo;
66+
sysPrefix: string;
67+
};
5468
export const IPythonExecutionService = Symbol('IPythonExecutionService');
5569

5670
export interface IPythonExecutionService {
71+
getInterpreterInformation(): Promise<InterpreterInfomation | undefined>;
5772
getExecutablePath(): Promise<string>;
5873
isModuleInstalled(moduleName: string): Promise<boolean>;
5974

src/client/common/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ export interface IPersistentState<T> {
2323
export const IPersistentStateFactory = Symbol('IPersistentStateFactory');
2424

2525
export interface IPersistentStateFactory {
26-
createGlobalPersistentState<T>(key: string, defaultValue: T): IPersistentState<T>;
27-
createWorkspacePersistentState<T>(key: string, defaultValue: T): IPersistentState<T>;
26+
createGlobalPersistentState<T>(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState<T>;
27+
createWorkspacePersistentState<T>(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState<T>;
2828
}
2929

3030
export type ExecutionInfo = {

src/client/debugger/configProviders/configurationProviderUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export class ConfigurationProviderUtils implements IConfigurationProviderUtils {
2424
}
2525
public async getPyramidStartupScriptFilePath(resource?: Uri): Promise<string | undefined> {
2626
try {
27-
const executionService = await this.executionFactory.create(resource);
27+
const executionService = await this.executionFactory.create({ resource });
2828
const output = await executionService.exec(['-c', 'import pyramid;print(pyramid.__file__)'], { throwOnStdErr: true });
2929
const pserveFilePath = path.join(path.dirname(output.stdout.trim()), 'scripts', PSERVE_SCRIPT_FILE_NAME);
3030
return await this.fs.fileExists(pserveFilePath) ? pserveFilePath : undefined;

src/client/interpreter/configuration/pythonPathUpdaterService.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { inject, injectable } from 'inversify';
22
import * as path from 'path';
33
import { ConfigurationTarget, Uri, window } from 'vscode';
4+
import { InterpreterInfomation, IPythonExecutionFactory } from '../../common/process/types';
45
import { StopWatch } from '../../common/stopWatch';
56
import { IServiceContainer } from '../../ioc/types';
67
import { sendTelemetryEvent } from '../../telemetry';
@@ -13,9 +14,11 @@ import { IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager } fr
1314
export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManager {
1415
private readonly pythonPathSettingsUpdaterFactory: IPythonPathUpdaterServiceFactory;
1516
private readonly interpreterVersionService: IInterpreterVersionService;
17+
private readonly executionFactory: IPythonExecutionFactory;
1618
constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
1719
this.pythonPathSettingsUpdaterFactory = serviceContainer.get<IPythonPathUpdaterServiceFactory>(IPythonPathUpdaterServiceFactory);
1820
this.interpreterVersionService = serviceContainer.get<IInterpreterVersionService>(IInterpreterVersionService);
21+
this.executionFactory = serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory);
1922
}
2023
public async updatePythonPath(pythonPath: string, configTarget: ConfigurationTarget, trigger: 'ui' | 'shebang' | 'load', wkspace?: Uri): Promise<void> {
2124
const stopWatch = new StopWatch();
@@ -39,17 +42,17 @@ export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManage
3942
failed, trigger
4043
};
4144
if (!failed) {
42-
const pyVersionPromise = this.interpreterVersionService.getVersion(pythonPath, '')
43-
.then(pyVersion => pyVersion.length === 0 ? undefined : pyVersion);
45+
const processService = await this.executionFactory.create({ pythonPath });
46+
const infoPromise = processService.getInterpreterInformation().catch<InterpreterInfomation>(() => undefined);
4447
const pipVersionPromise = this.interpreterVersionService.getPipVersion(pythonPath)
4548
.then(value => value.length === 0 ? undefined : value)
46-
.catch(() => undefined);
47-
const versions = await Promise.all([pyVersionPromise, pipVersionPromise]);
48-
if (versions[0]) {
49-
telemtryProperties.version = versions[0] as string;
49+
.catch<string>(() => undefined);
50+
const [info, pipVersion] = await Promise.all([infoPromise, pipVersionPromise]);
51+
if (info) {
52+
telemtryProperties.version = info.version;
5053
}
51-
if (versions[1]) {
52-
telemtryProperties.pipVersion = versions[1] as string;
54+
if (pipVersion) {
55+
telemtryProperties.pipVersion = pipVersion;
5356
}
5457
}
5558
sendTelemetryEvent(PYTHON_INTERPRETER, duration, telemtryProperties);

0 commit comments

Comments
 (0)