Skip to content

Commit 7d419ed

Browse files
authored
Add support for discovering python interpreters on windows using py.exe (microsoft#3783)
For microsoft#3369 * Depends on 3780 * Add support for discovering python interpreters on windows using py.exe
1 parent 56045bb commit 7d419ed

File tree

6 files changed

+150
-70
lines changed

6 files changed

+150
-70
lines changed

src/client/common/logger.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,6 @@ function trace(message: string, options: LogOptions = LogOptions.None, logLevel?
149149
})
150150
.catch(ex => {
151151
writeError(ex);
152-
return Promise.reject(ex);
153152
});
154153
} else {
155154
writeSuccess(result);

src/client/interpreter/interpreterService.ts

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { inject, injectable } from 'inversify';
22
import * as path from 'path';
33
import { ConfigurationTarget, Disposable, Event, EventEmitter, Uri } from 'vscode';
4+
import '../../client/common/extensions';
45
import { IDocumentManager, IWorkspaceService } from '../common/application/types';
56
import { PythonSettings } from '../common/configSettings';
67
import { getArchitectureDisplayName } from '../common/platform/registry';
78
import { IFileSystem } from '../common/platform/types';
89
import { IPythonExecutionFactory } from '../common/process/types';
910
import { IConfigurationService, IDisposableRegistry, IPersistentStateFactory } from '../common/types';
11+
import { sleep } from '../common/utils/async';
1012
import { IServiceContainer } from '../ioc/types';
1113
import { IPythonPathUpdaterServiceManager } from './configuration/types';
1214
import {
@@ -126,44 +128,54 @@ export class InterpreterService implements Disposable, IInterpreterService {
126128
}
127129
}
128130

129-
let fileHash = await this.fs.getFileHash(pythonPath).catch(() => '');
130-
fileHash = fileHash ? fileHash : '';
131+
const fileHash = await this.fs.getFileHash(pythonPath).catch(() => '') || '';
131132
const store = this.persistentStateFactory.createGlobalPersistentState<PythonInterpreter & { fileHash: string }>(`${pythonPath}.interpreter.details.v5`, undefined, EXPITY_DURATION);
132133
if (store.value && fileHash && store.value.fileHash === fileHash) {
133134
return store.value;
134135
}
135136

136137
const fs = this.serviceContainer.get<IFileSystem>(IFileSystem);
137-
const interpreters = await this.getInterpreters(resource);
138-
let interpreterInfo = interpreters.find(i => fs.arePathsSame(i.path, pythonPath));
139-
if (!interpreterInfo) {
140-
const interpreterHelper = this.serviceContainer.get<IInterpreterHelper>(IInterpreterHelper);
141-
const virtualEnvManager = this.serviceContainer.get<IVirtualEnvironmentManager>(IVirtualEnvironmentManager);
142-
const [info, type] = await Promise.all([
143-
interpreterHelper.getInterpreterInformation(pythonPath),
144-
virtualEnvManager.getEnvironmentType(pythonPath)
145-
]);
146-
if (!info) {
147-
return;
138+
139+
// Don't want for all interpreters are collected.
140+
// Try to collect the infromation manually, that's faster.
141+
// Get from which ever comes first.
142+
const option1 = (async () => {
143+
const result = this.collectInterpreterDetails(pythonPath, resource);
144+
await sleep(1000); // let the other option complete within 1s if possible.
145+
return result;
146+
})();
147+
148+
// This is the preferred approach, hence the delay in option 1.
149+
const option2 = (async () => {
150+
const interpreters = await this.getInterpreters(resource);
151+
const found = interpreters.find(i => fs.arePathsSame(i.path, pythonPath));
152+
if (found) {
153+
// Cache the interpreter info, only if we get the data from interpretr list.
154+
// tslint:disable-next-line:no-any
155+
(found as any).__store = true;
156+
return found;
148157
}
149-
const details: Partial<PythonInterpreter> = {
150-
...(info as PythonInterpreter),
151-
path: pythonPath,
152-
type: type
153-
};
158+
// Use option1 as a fallback.
159+
// tslint:disable-next-line:no-any
160+
return option1 as any as PythonInterpreter;
161+
})();
154162

155-
const envName = type === InterpreterType.Unknown ? undefined : await virtualEnvManager.getEnvironmentName(pythonPath, resource);
156-
interpreterInfo = {
157-
...(details as PythonInterpreter),
158-
envName
159-
};
160-
interpreterInfo.displayName = await this.getDisplayName(interpreterInfo, resource);
161-
}
163+
const interpreterInfo = await Promise.race([option2, option1]) as PythonInterpreter;
162164

163-
await store.updateValue({ ...interpreterInfo, path: pythonPath, fileHash });
165+
// tslint:disable-next-line:no-any
166+
if (interpreterInfo && (interpreterInfo as any).__store) {
167+
await store.updateValue({ ...interpreterInfo, path: pythonPath, fileHash });
168+
} else {
169+
// If we got information from option1, then when option2 finishes cache it for later use (ignoring erors);
170+
option2.then(info => {
171+
// tslint:disable-next-line:no-any
172+
if (info && (info as any).__store) {
173+
return store.updateValue({ ...info, path: pythonPath, fileHash });
174+
}
175+
}).ignoreErrors();
176+
}
164177
return interpreterInfo;
165178
}
166-
167179
/**
168180
* Gets the display name of an interpreter.
169181
* The format is `Python <Version> <bitness> (<env name>: <env type>)`
@@ -243,4 +255,28 @@ export class InterpreterService implements Disposable, IInterpreterService {
243255
interpreterDisplay.refresh()
244256
.catch(ex => console.error('Python Extension: display.refresh', ex));
245257
}
258+
private async collectInterpreterDetails(pythonPath: string, resource: Uri | undefined) {
259+
const interpreterHelper = this.serviceContainer.get<IInterpreterHelper>(IInterpreterHelper);
260+
const virtualEnvManager = this.serviceContainer.get<IVirtualEnvironmentManager>(IVirtualEnvironmentManager);
261+
const [info, type] = await Promise.all([
262+
interpreterHelper.getInterpreterInformation(pythonPath),
263+
virtualEnvManager.getEnvironmentType(pythonPath)
264+
]);
265+
if (!info) {
266+
return;
267+
}
268+
const details: Partial<PythonInterpreter> = {
269+
...(info as PythonInterpreter),
270+
path: pythonPath,
271+
type: type
272+
};
273+
274+
const envName = type === InterpreterType.Unknown ? undefined : await virtualEnvManager.getEnvironmentName(pythonPath, resource);
275+
const pthonInfo = {
276+
...(details as PythonInterpreter),
277+
envName
278+
};
279+
pthonInfo.displayName = await this.getDisplayName(pthonInfo, resource);
280+
return pthonInfo;
281+
}
246282
}

src/client/interpreter/locators/services/currentPathService.ts

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
// tslint:disable:no-require-imports no-var-requires underscore-consistent-invocation no-unnecessary-callback-wrapper
22
import { inject, injectable } from 'inversify';
33
import { Uri } from 'vscode';
4-
import { IFileSystem } from '../../../common/platform/types';
4+
import { traceError, traceInfo } from '../../../common/logger';
5+
import { IFileSystem, IPlatformService } from '../../../common/platform/types';
56
import { IProcessServiceFactory } from '../../../common/process/types';
67
import { IConfigurationService } from '../../../common/types';
8+
import { OSType } from '../../../common/utils/platform';
79
import { IServiceContainer } from '../../../ioc/types';
810
import { IInterpreterHelper, InterpreterType, PythonInterpreter } from '../../contracts';
11+
import { IPythonInPathCommandProvider } from '../types';
912
import { CacheableLocatorService } from './cacheableLocatorService';
10-
const flatten = require('lodash/flatten') as typeof import('lodash/flatten');
1113

1214
/**
1315
* Locates the currently configured Python interpreter.
@@ -22,6 +24,7 @@ export class CurrentPathService extends CacheableLocatorService {
2224
public constructor(
2325
@inject(IInterpreterHelper) private helper: IInterpreterHelper,
2426
@inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory,
27+
@inject(IPythonInPathCommandProvider) private readonly pythonCommandProvider: IPythonInPathCommandProvider,
2528
@inject(IServiceContainer) serviceContainer: IServiceContainer
2629
) {
2730
super('CurrentPathService', serviceContainer);
@@ -50,12 +53,10 @@ export class CurrentPathService extends CacheableLocatorService {
5053
*/
5154
private async suggestionsFromKnownPaths(resource?: Uri) {
5255
const configSettings = this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(resource);
53-
const currentPythonInterpreter = this.getInterpreter(configSettings.pythonPath, '').then(interpreter => [interpreter]);
54-
const python3 = this.getInterpreter('python3', '').then(interpreter => [interpreter]);
55-
const python2 = this.getInterpreter('python2', '').then(interpreter => [interpreter]);
56-
const python = this.getInterpreter('python', '').then(interpreter => [interpreter]);
57-
return Promise.all<string[]>([currentPythonInterpreter, python3, python2, python])
58-
.then(listOfInterpreters => flatten(listOfInterpreters))
56+
const pathsToCheck = [...this.pythonCommandProvider.getCommands(), { command: configSettings.pythonPath }];
57+
58+
const pythonPaths = Promise.all(pathsToCheck.map(item => this.getInterpreter(item)));
59+
return pythonPaths
5960
.then(interpreters => interpreters.filter(item => item.length > 0))
6061
// tslint:disable-next-line:promise-function-async
6162
.then(interpreters => Promise.all(interpreters.map(interpreter => this.getInterpreterDetails(interpreter))))
@@ -65,15 +66,15 @@ export class CurrentPathService extends CacheableLocatorService {
6566
/**
6667
* Return the information about the identified interpreter binary.
6768
*/
68-
private async getInterpreterDetails(interpreter: string): Promise<PythonInterpreter | undefined> {
69-
return this.helper.getInterpreterInformation(interpreter)
69+
private async getInterpreterDetails(pythonPath: string): Promise<PythonInterpreter | undefined> {
70+
return this.helper.getInterpreterInformation(pythonPath)
7071
.then(details => {
7172
if (!details) {
7273
return;
7374
}
7475
return {
7576
...(details as PythonInterpreter),
76-
path: interpreter,
77+
path: pythonPath,
7778
type: details.type ? details.type : InterpreterType.Unknown
7879
};
7980
});
@@ -82,20 +83,43 @@ export class CurrentPathService extends CacheableLocatorService {
8283
/**
8384
* Return the path to the interpreter (or the default if not found).
8485
*/
85-
private async getInterpreter(pythonPath: string, defaultValue: string) {
86+
private async getInterpreter(options: { command: string; args?: string[] }) {
8687
try {
8788
const processService = await this.processServiceFactory.create();
88-
return processService.exec(pythonPath, ['-c', 'import sys;print(sys.executable)'], {})
89+
const args = Array.isArray(options.args) ? options.args : [];
90+
return processService.exec(options.command, args.concat(['-c', 'import sys;print(sys.executable)']), {})
8991
.then(output => output.stdout.trim())
9092
.then(async value => {
9193
if (value.length > 0 && await this.fs.fileExists(value)) {
9294
return value;
9395
}
94-
return defaultValue;
96+
traceError(`Detection of Python Interpreter for Command ${options.command} and args ${args.join(' ')} failed as file ${value} does not exist`);
97+
return '';
9598
})
96-
.catch(() => defaultValue); // Ignore exceptions in getting the executable.
97-
} catch {
98-
return defaultValue; // Ignore exceptions in getting the executable.
99+
.catch(ex => {
100+
traceInfo(`Detection of Python Interpreter for Command ${options.command} and args ${args.join(' ')} failed`);
101+
return '';
102+
}); // Ignore exceptions in getting the executable.
103+
} catch (ex) {
104+
traceError(`Detection of Python Interpreter for Command ${options.command} failed`, ex);
105+
return ''; // Ignore exceptions in getting the executable.
99106
}
100107
}
101108
}
109+
110+
@injectable()
111+
export class PythonInPathCommandProvider implements IPythonInPathCommandProvider {
112+
constructor(@inject(IPlatformService) private readonly platform: IPlatformService) { }
113+
public getCommands(): { command: string; args?: string[] }[] {
114+
const paths = ['python3.7', 'python3.6', 'python3', 'python2', 'python']
115+
.map(item => { return { command: item }; });
116+
if (this.platform.osType !== OSType.Windows) {
117+
return paths;
118+
}
119+
120+
const versions = ['3.7', '3.6', '3', '2'];
121+
return paths.concat(versions.map(version => {
122+
return { command: 'py', args: [`-${version}`] };
123+
}));
124+
}
125+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
export const IPythonInPathCommandProvider = Symbol('IPythonInPathCommandProvider');
7+
export interface IPythonInPathCommandProvider {
8+
getCommands(): { command: string; args?: string[] }[];
9+
}

src/client/interpreter/serviceRegistry.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,15 @@ import { InterpreterLocatorProgressService } from './locators/progressService';
4646
import { CondaEnvFileService } from './locators/services/condaEnvFileService';
4747
import { CondaEnvService } from './locators/services/condaEnvService';
4848
import { CondaService } from './locators/services/condaService';
49-
import { CurrentPathService } from './locators/services/currentPathService';
49+
import { CurrentPathService, PythonInPathCommandProvider } from './locators/services/currentPathService';
5050
import { GlobalVirtualEnvironmentsSearchPathProvider, GlobalVirtualEnvService } from './locators/services/globalVirtualEnvService';
5151
import { InterpreterWatcherBuilder } from './locators/services/interpreterWatcherBuilder';
5252
import { KnownPathsService, KnownSearchPathsForInterpreters } from './locators/services/KnownPathsService';
5353
import { PipEnvService } from './locators/services/pipEnvService';
5454
import { WindowsRegistryService } from './locators/services/windowsRegistryService';
5555
import { WorkspaceVirtualEnvironmentsSearchPathProvider, WorkspaceVirtualEnvService } from './locators/services/workspaceVirtualEnvService';
5656
import { WorkspaceVirtualEnvWatcherService } from './locators/services/workspaceVirtualEnvWatcherService';
57+
import { IPythonInPathCommandProvider } from './locators/types';
5758
import { VirtualEnvironmentManager } from './virtualEnvs/index';
5859
import { IVirtualEnvironmentManager } from './virtualEnvs/types';
5960

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

6566
serviceManager.addSingleton<ICondaService>(ICondaService, CondaService);
6667
serviceManager.addSingleton<IVirtualEnvironmentManager>(IVirtualEnvironmentManager, VirtualEnvironmentManager);
68+
serviceManager.addSingleton<IPythonInPathCommandProvider>(IPythonInPathCommandProvider, PythonInPathCommandProvider);
6769

6870
serviceManager.add<IInterpreterWatcher>(IInterpreterWatcher, WorkspaceVirtualEnvWatcherService, WORKSPACE_VIRTUAL_ENV_SERVICE);
6971
serviceManager.addSingleton<IInterpreterWatcherBuilder>(IInterpreterWatcherBuilder, InterpreterWatcherBuilder);

0 commit comments

Comments
 (0)