Skip to content

Commit

Permalink
Add support for discovering python interpreters on windows using py.e…
Browse files Browse the repository at this point in the history
…xe (#3783)

For #3369

* Depends on 3780
* Add support for discovering python interpreters on windows using py.exe
  • Loading branch information
DonJayamanne committed Dec 22, 2018
1 parent 56045bb commit 7d419ed
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 70 deletions.
1 change: 0 additions & 1 deletion src/client/common/logger.ts
Expand Up @@ -149,7 +149,6 @@ function trace(message: string, options: LogOptions = LogOptions.None, logLevel?
})
.catch(ex => {
writeError(ex);
return Promise.reject(ex);
});
} else {
writeSuccess(result);
Expand Down
90 changes: 63 additions & 27 deletions src/client/interpreter/interpreterService.ts
@@ -1,12 +1,14 @@
import { inject, injectable } from 'inversify';
import * as path from 'path';
import { ConfigurationTarget, Disposable, Event, EventEmitter, Uri } from 'vscode';
import '../../client/common/extensions';
import { IDocumentManager, IWorkspaceService } from '../common/application/types';
import { PythonSettings } from '../common/configSettings';
import { getArchitectureDisplayName } from '../common/platform/registry';
import { IFileSystem } from '../common/platform/types';
import { IPythonExecutionFactory } from '../common/process/types';
import { IConfigurationService, IDisposableRegistry, IPersistentStateFactory } from '../common/types';
import { sleep } from '../common/utils/async';
import { IServiceContainer } from '../ioc/types';
import { IPythonPathUpdaterServiceManager } from './configuration/types';
import {
Expand Down Expand Up @@ -126,44 +128,54 @@ export class InterpreterService implements Disposable, IInterpreterService {
}
}

let fileHash = await this.fs.getFileHash(pythonPath).catch(() => '');
fileHash = fileHash ? fileHash : '';
const fileHash = await this.fs.getFileHash(pythonPath).catch(() => '') || '';
const store = this.persistentStateFactory.createGlobalPersistentState<PythonInterpreter & { fileHash: string }>(`${pythonPath}.interpreter.details.v5`, undefined, EXPITY_DURATION);
if (store.value && fileHash && store.value.fileHash === fileHash) {
return store.value;
}

const fs = this.serviceContainer.get<IFileSystem>(IFileSystem);
const interpreters = await this.getInterpreters(resource);
let interpreterInfo = interpreters.find(i => fs.arePathsSame(i.path, pythonPath));
if (!interpreterInfo) {
const interpreterHelper = this.serviceContainer.get<IInterpreterHelper>(IInterpreterHelper);
const virtualEnvManager = this.serviceContainer.get<IVirtualEnvironmentManager>(IVirtualEnvironmentManager);
const [info, type] = await Promise.all([
interpreterHelper.getInterpreterInformation(pythonPath),
virtualEnvManager.getEnvironmentType(pythonPath)
]);
if (!info) {
return;

// Don't want for all interpreters are collected.
// Try to collect the infromation manually, that's faster.
// Get from which ever comes first.
const option1 = (async () => {
const result = this.collectInterpreterDetails(pythonPath, resource);
await sleep(1000); // let the other option complete within 1s if possible.
return result;
})();

// This is the preferred approach, hence the delay in option 1.
const option2 = (async () => {
const interpreters = await this.getInterpreters(resource);
const found = interpreters.find(i => fs.arePathsSame(i.path, pythonPath));
if (found) {
// Cache the interpreter info, only if we get the data from interpretr list.
// tslint:disable-next-line:no-any
(found as any).__store = true;
return found;
}
const details: Partial<PythonInterpreter> = {
...(info as PythonInterpreter),
path: pythonPath,
type: type
};
// Use option1 as a fallback.
// tslint:disable-next-line:no-any
return option1 as any as PythonInterpreter;
})();

const envName = type === InterpreterType.Unknown ? undefined : await virtualEnvManager.getEnvironmentName(pythonPath, resource);
interpreterInfo = {
...(details as PythonInterpreter),
envName
};
interpreterInfo.displayName = await this.getDisplayName(interpreterInfo, resource);
}
const interpreterInfo = await Promise.race([option2, option1]) as PythonInterpreter;

await store.updateValue({ ...interpreterInfo, path: pythonPath, fileHash });
// tslint:disable-next-line:no-any
if (interpreterInfo && (interpreterInfo as any).__store) {
await store.updateValue({ ...interpreterInfo, path: pythonPath, fileHash });
} else {
// If we got information from option1, then when option2 finishes cache it for later use (ignoring erors);
option2.then(info => {
// tslint:disable-next-line:no-any
if (info && (info as any).__store) {
return store.updateValue({ ...info, path: pythonPath, fileHash });
}
}).ignoreErrors();
}
return interpreterInfo;
}

/**
* Gets the display name of an interpreter.
* The format is `Python <Version> <bitness> (<env name>: <env type>)`
Expand Down Expand Up @@ -243,4 +255,28 @@ export class InterpreterService implements Disposable, IInterpreterService {
interpreterDisplay.refresh()
.catch(ex => console.error('Python Extension: display.refresh', ex));
}
private async collectInterpreterDetails(pythonPath: string, resource: Uri | undefined) {
const interpreterHelper = this.serviceContainer.get<IInterpreterHelper>(IInterpreterHelper);
const virtualEnvManager = this.serviceContainer.get<IVirtualEnvironmentManager>(IVirtualEnvironmentManager);
const [info, type] = await Promise.all([
interpreterHelper.getInterpreterInformation(pythonPath),
virtualEnvManager.getEnvironmentType(pythonPath)
]);
if (!info) {
return;
}
const details: Partial<PythonInterpreter> = {
...(info as PythonInterpreter),
path: pythonPath,
type: type
};

const envName = type === InterpreterType.Unknown ? undefined : await virtualEnvManager.getEnvironmentName(pythonPath, resource);
const pthonInfo = {
...(details as PythonInterpreter),
envName
};
pthonInfo.displayName = await this.getDisplayName(pthonInfo, resource);
return pthonInfo;
}
}
58 changes: 41 additions & 17 deletions src/client/interpreter/locators/services/currentPathService.ts
@@ -1,13 +1,15 @@
// tslint:disable:no-require-imports no-var-requires underscore-consistent-invocation no-unnecessary-callback-wrapper
import { inject, injectable } from 'inversify';
import { Uri } from 'vscode';
import { IFileSystem } from '../../../common/platform/types';
import { traceError, traceInfo } from '../../../common/logger';
import { IFileSystem, IPlatformService } from '../../../common/platform/types';
import { IProcessServiceFactory } from '../../../common/process/types';
import { IConfigurationService } from '../../../common/types';
import { OSType } from '../../../common/utils/platform';
import { IServiceContainer } from '../../../ioc/types';
import { IInterpreterHelper, InterpreterType, PythonInterpreter } from '../../contracts';
import { IPythonInPathCommandProvider } from '../types';
import { CacheableLocatorService } from './cacheableLocatorService';
const flatten = require('lodash/flatten') as typeof import('lodash/flatten');

/**
* Locates the currently configured Python interpreter.
Expand All @@ -22,6 +24,7 @@ export class CurrentPathService extends CacheableLocatorService {
public constructor(
@inject(IInterpreterHelper) private helper: IInterpreterHelper,
@inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory,
@inject(IPythonInPathCommandProvider) private readonly pythonCommandProvider: IPythonInPathCommandProvider,
@inject(IServiceContainer) serviceContainer: IServiceContainer
) {
super('CurrentPathService', serviceContainer);
Expand Down Expand Up @@ -50,12 +53,10 @@ export class CurrentPathService extends CacheableLocatorService {
*/
private async suggestionsFromKnownPaths(resource?: Uri) {
const configSettings = this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(resource);
const currentPythonInterpreter = this.getInterpreter(configSettings.pythonPath, '').then(interpreter => [interpreter]);
const python3 = this.getInterpreter('python3', '').then(interpreter => [interpreter]);
const python2 = this.getInterpreter('python2', '').then(interpreter => [interpreter]);
const python = this.getInterpreter('python', '').then(interpreter => [interpreter]);
return Promise.all<string[]>([currentPythonInterpreter, python3, python2, python])
.then(listOfInterpreters => flatten(listOfInterpreters))
const pathsToCheck = [...this.pythonCommandProvider.getCommands(), { command: configSettings.pythonPath }];

const pythonPaths = Promise.all(pathsToCheck.map(item => this.getInterpreter(item)));
return pythonPaths
.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))))
Expand All @@ -65,15 +66,15 @@ export class CurrentPathService extends CacheableLocatorService {
/**
* Return the information about the identified interpreter binary.
*/
private async getInterpreterDetails(interpreter: string): Promise<PythonInterpreter | undefined> {
return this.helper.getInterpreterInformation(interpreter)
private async getInterpreterDetails(pythonPath: string): Promise<PythonInterpreter | undefined> {
return this.helper.getInterpreterInformation(pythonPath)
.then(details => {
if (!details) {
return;
}
return {
...(details as PythonInterpreter),
path: interpreter,
path: pythonPath,
type: details.type ? details.type : InterpreterType.Unknown
};
});
Expand All @@ -82,20 +83,43 @@ export class CurrentPathService extends CacheableLocatorService {
/**
* Return the path to the interpreter (or the default if not found).
*/
private async getInterpreter(pythonPath: string, defaultValue: string) {
private async getInterpreter(options: { command: string; args?: string[] }) {
try {
const processService = await this.processServiceFactory.create();
return processService.exec(pythonPath, ['-c', 'import sys;print(sys.executable)'], {})
const args = Array.isArray(options.args) ? options.args : [];
return processService.exec(options.command, args.concat(['-c', 'import sys;print(sys.executable)']), {})
.then(output => output.stdout.trim())
.then(async value => {
if (value.length > 0 && await this.fs.fileExists(value)) {
return value;
}
return defaultValue;
traceError(`Detection of Python Interpreter for Command ${options.command} and args ${args.join(' ')} failed as file ${value} does not exist`);
return '';
})
.catch(() => defaultValue); // Ignore exceptions in getting the executable.
} catch {
return defaultValue; // Ignore exceptions in getting the executable.
.catch(ex => {
traceInfo(`Detection of Python Interpreter for Command ${options.command} and args ${args.join(' ')} failed`);
return '';
}); // Ignore exceptions in getting the executable.
} catch (ex) {
traceError(`Detection of Python Interpreter for Command ${options.command} failed`, ex);
return ''; // Ignore exceptions in getting the executable.
}
}
}

@injectable()
export class PythonInPathCommandProvider implements IPythonInPathCommandProvider {
constructor(@inject(IPlatformService) private readonly platform: IPlatformService) { }
public getCommands(): { command: string; args?: string[] }[] {
const paths = ['python3.7', 'python3.6', 'python3', 'python2', 'python']
.map(item => { return { command: item }; });
if (this.platform.osType !== OSType.Windows) {
return paths;
}

const versions = ['3.7', '3.6', '3', '2'];
return paths.concat(versions.map(version => {
return { command: 'py', args: [`-${version}`] };
}));
}
}
9 changes: 9 additions & 0 deletions src/client/interpreter/locators/types.ts
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

export const IPythonInPathCommandProvider = Symbol('IPythonInPathCommandProvider');
export interface IPythonInPathCommandProvider {
getCommands(): { command: string; args?: string[] }[];
}
4 changes: 3 additions & 1 deletion src/client/interpreter/serviceRegistry.ts
Expand Up @@ -46,14 +46,15 @@ import { InterpreterLocatorProgressService } from './locators/progressService';
import { CondaEnvFileService } from './locators/services/condaEnvFileService';
import { CondaEnvService } from './locators/services/condaEnvService';
import { CondaService } from './locators/services/condaService';
import { CurrentPathService } from './locators/services/currentPathService';
import { CurrentPathService, PythonInPathCommandProvider } from './locators/services/currentPathService';
import { GlobalVirtualEnvironmentsSearchPathProvider, GlobalVirtualEnvService } from './locators/services/globalVirtualEnvService';
import { InterpreterWatcherBuilder } from './locators/services/interpreterWatcherBuilder';
import { KnownPathsService, KnownSearchPathsForInterpreters } from './locators/services/KnownPathsService';
import { PipEnvService } from './locators/services/pipEnvService';
import { WindowsRegistryService } from './locators/services/windowsRegistryService';
import { WorkspaceVirtualEnvironmentsSearchPathProvider, WorkspaceVirtualEnvService } from './locators/services/workspaceVirtualEnvService';
import { WorkspaceVirtualEnvWatcherService } from './locators/services/workspaceVirtualEnvWatcherService';
import { IPythonInPathCommandProvider } from './locators/types';
import { VirtualEnvironmentManager } from './virtualEnvs/index';
import { IVirtualEnvironmentManager } from './virtualEnvs/types';

Expand All @@ -64,6 +65,7 @@ export function registerTypes(serviceManager: IServiceManager) {

serviceManager.addSingleton<ICondaService>(ICondaService, CondaService);
serviceManager.addSingleton<IVirtualEnvironmentManager>(IVirtualEnvironmentManager, VirtualEnvironmentManager);
serviceManager.addSingleton<IPythonInPathCommandProvider>(IPythonInPathCommandProvider, PythonInPathCommandProvider);

serviceManager.add<IInterpreterWatcher>(IInterpreterWatcher, WorkspaceVirtualEnvWatcherService, WORKSPACE_VIRTUAL_ENV_SERVICE);
serviceManager.addSingleton<IInterpreterWatcherBuilder>(IInterpreterWatcherBuilder, InterpreterWatcherBuilder);
Expand Down

0 comments on commit 7d419ed

Please sign in to comment.