Skip to content

Commit 6c80bbc

Browse files
authored
Changes to logic for selection of best (default) version of Python interpreter (microsoft#3813)
* Refactor version in interpreter information * News entry * Add telemetry * Changed version to a simple struct * Tests for config settings * Fixed linter command tests * Create linter manage unit tests
1 parent 7e0c9e8 commit 6c80bbc

File tree

81 files changed

+3010
-985
lines changed

Some content is hidden

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

81 files changed

+3010
-985
lines changed

build/ci/mocha-vsts-reporter.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,9 @@ function MochaVstsReporter(runner, options) {
1515

1616
runner.on('suite', function(suite){
1717
if (suite.root === true){
18-
console.log('Begin test run.............');
1918
indentLevel++;
2019
indenter = INDENT_BASE.repeat(indentLevel);
2120
} else {
22-
console.log('%sStart "%s"', indenter, suite.title);
2321
indentLevel++;
2422
indenter = INDENT_BASE.repeat(indentLevel);
2523
}
@@ -29,9 +27,7 @@ function MochaVstsReporter(runner, options) {
2927
if (suite.root === true) {
3028
indentLevel=0;
3129
indenter = '';
32-
console.log('.............End test run.');
3330
} else {
34-
console.log('%sEnd "%s"', indenter, suite.title);
3531
indentLevel--;
3632
indenter = INDENT_BASE.repeat(indentLevel);
3733
// ##vso[task.setprogress]current operation

news/1 Enhancements/3369.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improvements to automatic selection of the python interpreter.

src/client/common/configSettings.ts

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
import * as child_process from 'child_process';
44
import { EventEmitter } from 'events';
55
import * as path from 'path';
6-
import {
7-
ConfigurationChangeEvent, ConfigurationTarget, DiagnosticSeverity, Disposable, Uri,
8-
workspace, WorkspaceConfiguration
9-
} from 'vscode';
6+
import { ConfigurationChangeEvent, ConfigurationTarget, DiagnosticSeverity, Disposable, Uri, WorkspaceConfiguration } from 'vscode';
7+
import '../common/extensions';
8+
import { IInterpreterAutoSeletionProxyService } from '../interpreter/autoSelection/types';
109
import { sendTelemetryEvent } from '../telemetry';
1110
import { COMPLETION_ADD_BRACKETS, FORMAT_ON_TYPE } from '../telemetry/constants';
11+
import { IWorkspaceService } from './application/types';
12+
import { WorkspaceService } from './application/workspace';
1213
import { isTestExecution } from './constants';
1314
import { IS_WINDOWS } from './platform/constants';
1415
import {
@@ -57,21 +58,28 @@ export class PythonSettings extends EventEmitter implements IPythonSettings {
5758
private disposables: Disposable[] = [];
5859
// tslint:disable-next-line:variable-name
5960
private _pythonPath = '';
61+
private readonly workspace: IWorkspaceService;
6062

61-
constructor(workspaceFolder?: Uri) {
63+
constructor(workspaceFolder: Uri | undefined, private readonly InterpreterAutoSelectionService: IInterpreterAutoSeletionProxyService,
64+
workspace?: IWorkspaceService) {
6265
super();
66+
this.workspace = workspace || new WorkspaceService();
6367
this.workspaceRoot = workspaceFolder ? workspaceFolder : Uri.file(__dirname);
6468
this.initialize();
6569
}
6670
// tslint:disable-next-line:function-name
67-
public static getInstance(resource?: Uri): PythonSettings {
68-
const workspaceFolderUri = PythonSettings.getSettingsUriAndTarget(resource).uri;
71+
public static getInstance(resource: Uri | undefined, interpreterAutoSelectionService: IInterpreterAutoSeletionProxyService,
72+
workspace?: IWorkspaceService): PythonSettings {
73+
workspace = workspace || new WorkspaceService();
74+
const workspaceFolderUri = PythonSettings.getSettingsUriAndTarget(resource, workspace).uri;
6975
const workspaceFolderKey = workspaceFolderUri ? workspaceFolderUri.fsPath : '';
7076

7177
if (!PythonSettings.pythonSettings.has(workspaceFolderKey)) {
72-
const settings = new PythonSettings(workspaceFolderUri);
78+
const settings = new PythonSettings(workspaceFolderUri, interpreterAutoSelectionService, workspace);
7379
PythonSettings.pythonSettings.set(workspaceFolderKey, settings);
74-
const config = workspace.getConfiguration('editor', resource ? resource : null);
80+
// Pass null to avoid VSC from complaining about not passing in a value.
81+
// tslint:disable-next-line:no-any
82+
const config = workspace.getConfiguration('editor', resource ? resource : null as any);
7583
const formatOnType = config ? config.get('formatOnType', false) : false;
7684
sendTelemetryEvent(COMPLETION_ADD_BRACKETS, undefined, { enabled: settings.autoComplete ? settings.autoComplete.addBrackets : false });
7785
sendTelemetryEvent(FORMAT_ON_TYPE, undefined, { enabled: formatOnType });
@@ -81,7 +89,8 @@ export class PythonSettings extends EventEmitter implements IPythonSettings {
8189
}
8290

8391
// tslint:disable-next-line:type-literal-delimiter
84-
public static getSettingsUriAndTarget(resource?: Uri): { uri: Uri | undefined, target: ConfigurationTarget } {
92+
public static getSettingsUriAndTarget(resource: Uri | undefined, workspace?: IWorkspaceService): { uri: Uri | undefined, target: ConfigurationTarget } {
93+
workspace = workspace || new WorkspaceService();
8594
const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined;
8695
let workspaceFolderUri: Uri | undefined = workspaceFolder ? workspaceFolder.uri : undefined;
8796

@@ -99,21 +108,29 @@ export class PythonSettings extends EventEmitter implements IPythonSettings {
99108
throw new Error('Dispose can only be called from unit tests');
100109
}
101110
// tslint:disable-next-line:no-void-expression
102-
PythonSettings.pythonSettings.forEach(item => item.dispose());
111+
PythonSettings.pythonSettings.forEach(item => item && item.dispose());
103112
PythonSettings.pythonSettings.clear();
104113
}
105114
public dispose() {
106115
// tslint:disable-next-line:no-unsafe-any
107-
this.disposables.forEach(disposable => disposable.dispose());
116+
this.disposables.forEach(disposable => disposable && disposable.dispose());
108117
this.disposables = [];
109118
}
110119
// tslint:disable-next-line:cyclomatic-complexity max-func-body-length
111-
public update(pythonSettings: WorkspaceConfiguration) {
120+
protected update(pythonSettings: WorkspaceConfiguration) {
112121
const workspaceRoot = this.workspaceRoot.fsPath;
113122
const systemVariables: SystemVariables = new SystemVariables(this.workspaceRoot ? this.workspaceRoot.fsPath : undefined);
114123

115124
// tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion
116125
this.pythonPath = systemVariables.resolveAny(pythonSettings.get<string>('pythonPath'))!;
126+
// If user has defined a custom value, use it else try to get the best interpreter ourselves.
127+
if (this.pythonPath.length === 0 || this.pythonPath === 'python') {
128+
const autoSelectedPythonInterpreter = this.InterpreterAutoSelectionService.getAutoSelectedInterpreter(this.workspaceRoot);
129+
if (autoSelectedPythonInterpreter) {
130+
this.InterpreterAutoSelectionService.setWorkspaceInterpreter(this.workspaceRoot, autoSelectedPythonInterpreter).ignoreErrors();
131+
}
132+
this.pythonPath = autoSelectedPythonInterpreter ? autoSelectedPythonInterpreter.path : this.pythonPath;
133+
}
117134
this.pythonPath = getAbsolutePath(this.pythonPath, workspaceRoot);
118135
// tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion
119136
this.venvPath = systemVariables.resolveAny(pythonSettings.get<string>('venvPath'))!;
@@ -342,25 +359,31 @@ export class PythonSettings extends EventEmitter implements IPythonSettings {
342359
// Add support for specifying just the directory where the python executable will be located.
343360
// E.g. virtual directory name.
344361
try {
345-
this._pythonPath = getPythonExecutable(value);
362+
this._pythonPath = this.getPythonExecutable(value);
346363
} catch (ex) {
347364
this._pythonPath = value;
348365
}
349366
}
367+
protected getPythonExecutable(pythonPath: string) {
368+
return getPythonExecutable(pythonPath);
369+
}
350370
protected initialize(): void {
351-
this.disposables.push(workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => {
352-
if (event.affectsConfiguration('python'))
353-
{
354-
const currentConfig = workspace.getConfiguration('python', this.workspaceRoot);
355-
this.update(currentConfig);
356-
357-
// If workspace config changes, then we could have a cascading effect of on change events.
358-
// Let's defer the change notification.
359-
setTimeout(() => this.emit('change'), 1);
371+
const onDidChange = () => {
372+
const currentConfig = this.workspace.getConfiguration('python', this.workspaceRoot);
373+
this.update(currentConfig);
374+
375+
// If workspace config changes, then we could have a cascading effect of on change events.
376+
// Let's defer the change notification.
377+
setTimeout(() => this.emit('change'), 1);
378+
};
379+
this.disposables.push(this.InterpreterAutoSelectionService.onDidChangeAutoSelectedInterpreter(onDidChange.bind(this)));
380+
this.disposables.push(this.workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => {
381+
if (event.affectsConfiguration('python')) {
382+
onDidChange();
360383
}
361384
}));
362385

363-
const initialConfig = workspace.getConfiguration('python', this.workspaceRoot);
386+
const initialConfig = this.workspace.getConfiguration('python', this.workspaceRoot);
364387
if (initialConfig) {
365388
this.update(initialConfig);
366389
}

src/client/common/configuration/service.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,34 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
import { injectable } from 'inversify';
4+
import { inject, injectable } from 'inversify';
55
import { ConfigurationTarget, Uri, workspace, WorkspaceConfiguration } from 'vscode';
6+
import { IInterpreterAutoSeletionProxyService } from '../../interpreter/autoSelection/types';
7+
import { IServiceContainer } from '../../ioc/types';
8+
import { IWorkspaceService } from '../application/types';
69
import { PythonSettings } from '../configSettings';
710
import { IConfigurationService, IPythonSettings } from '../types';
811

912
@injectable()
1013
export class ConfigurationService implements IConfigurationService {
14+
private readonly workspaceService: IWorkspaceService;
15+
constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) {
16+
this.workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService);
17+
}
1118
public getSettings(resource?: Uri): IPythonSettings {
12-
return PythonSettings.getInstance(resource);
19+
const InterpreterAutoSelectionService = this.serviceContainer.get<IInterpreterAutoSeletionProxyService>(IInterpreterAutoSeletionProxyService);
20+
return PythonSettings.getInstance(resource, InterpreterAutoSelectionService, this.workspaceService);
1321
}
1422

1523
public async updateSectionSetting(section: string, setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise<void> {
1624
const defaultSetting = {
1725
uri: resource,
1826
target: configTarget || ConfigurationTarget.WorkspaceFolder
1927
};
20-
const settingsInfo = section === 'python' && configTarget !== ConfigurationTarget.Global ? PythonSettings.getSettingsUriAndTarget(resource) : defaultSetting;
28+
let settingsInfo = defaultSetting;
29+
if (section === 'python' && configTarget !== ConfigurationTarget.Global) {
30+
settingsInfo = PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService);
31+
}
2132

2233
const configSection = workspace.getConfiguration(section, settingsInfo.uri);
2334
const currentValue = configSection.inspect(setting);

src/client/common/installer/productInstaller.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -197,16 +197,16 @@ export class LinterInstaller extends BaseInstaller {
197197
}
198198
const response = await this.appShell.showErrorMessage(message, ...options);
199199
if (response === install) {
200-
sendTelemetryEvent(LINTER_NOT_INSTALLED_PROMPT, undefined, { tool: productName as LinterId, action: 'install'});
200+
sendTelemetryEvent(LINTER_NOT_INSTALLED_PROMPT, undefined, { tool: productName as LinterId, action: 'install' });
201201
return this.install(product, resource);
202202
} else if (response === disableInstallPrompt) {
203203
await this.setStoredResponse(disableLinterInstallPromptKey, true);
204-
sendTelemetryEvent(LINTER_NOT_INSTALLED_PROMPT, undefined, { tool: productName as LinterId, action: 'disablePrompt'});
204+
sendTelemetryEvent(LINTER_NOT_INSTALLED_PROMPT, undefined, { tool: productName as LinterId, action: 'disablePrompt' });
205205
return InstallerResponse.Ignore;
206206
}
207207

208-
if (response === selectLinter){
209-
sendTelemetryEvent(LINTER_NOT_INSTALLED_PROMPT, undefined, { action: 'select'});
208+
if (response === selectLinter) {
209+
sendTelemetryEvent(LINTER_NOT_INSTALLED_PROMPT, undefined, { action: 'select' });
210210
const commandManager = this.serviceContainer.get<ICommandManager>(ICommandManager);
211211
await commandManager.executeCommand(Commands.Set_Linter);
212212
}
@@ -224,7 +224,7 @@ export class LinterInstaller extends BaseInstaller {
224224
protected getStoredResponse(key: string): boolean {
225225
const factory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory);
226226
const state = factory.createGlobalPersistentState<boolean | undefined>(key, undefined);
227-
return state.value;
227+
return state.value === true;
228228
}
229229

230230
/**

src/client/common/persistentState.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { inject, injectable, named } from 'inversify';
77
import { Memento } from 'vscode';
88
import { GLOBAL_MEMENTO, IMemento, IPersistentState, IPersistentStateFactory, WORKSPACE_MEMENTO } from './types';
99

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

1313
public get value(): T {

src/client/common/platform/registry.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { injectable } from 'inversify';
2-
import * as Registry from 'winreg';
2+
import { Options } from 'winreg';
33
import { Architecture } from '../utils/platform';
44
import { IRegistry, RegistryHive } from './types';
55

@@ -29,7 +29,9 @@ export function getArchitectureDisplayName(arch?: Architecture) {
2929
}
3030
}
3131

32-
async function getRegistryValue(options: Registry.Options, name: string = '') {
32+
async function getRegistryValue(options: Options, name: string = '') {
33+
// tslint:disable-next-line:no-require-imports
34+
const Registry = require('winreg') as typeof import('winreg');
3335
return new Promise<string | undefined | null>((resolve, reject) => {
3436
new Registry(options).get(name, (error, result) => {
3537
if (error || !result || typeof result.value !== 'string') {
@@ -39,7 +41,10 @@ async function getRegistryValue(options: Registry.Options, name: string = '') {
3941
});
4042
});
4143
}
42-
async function getRegistryKeys(options: Registry.Options): Promise<string[]> {
44+
45+
async function getRegistryKeys(options: Options): Promise<string[]> {
46+
// tslint:disable-next-line:no-require-imports
47+
const Registry = require('winreg') as typeof import('winreg');
4348
// https://github.com/python/peps/blob/master/pep-0514.txt#L85
4449
return new Promise<string[]>((resolve, reject) => {
4550
new Registry(options).keys((error, result) => {
@@ -61,6 +66,8 @@ function translateArchitecture(arch?: Architecture): RegistryArchitectures | und
6166
}
6267
}
6368
function translateHive(hive: RegistryHive): string | undefined {
69+
// tslint:disable-next-line:no-require-imports
70+
const Registry = require('winreg') as typeof import('winreg');
6471
switch (hive) {
6572
case RegistryHive.HKCU:
6673
return Registry.HKCU;

src/client/common/platform/serviceRegistry.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,5 @@ import { IFileSystem, IPlatformService, IRegistry } from './types';
1111
export function registerTypes(serviceManager: IServiceManager) {
1212
serviceManager.addSingleton<IPlatformService>(IPlatformService, PlatformService);
1313
serviceManager.addSingleton<IFileSystem>(IFileSystem, FileSystem);
14-
if (serviceManager.get<IPlatformService>(IPlatformService).isWindows) {
15-
serviceManager.addSingleton<IRegistry>(IRegistry, RegistryImplementation);
16-
}
14+
serviceManager.addSingleton<IRegistry>(IRegistry, RegistryImplementation);
1715
}

src/client/common/process/pythonProcess.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { ModuleNotInstalledError } from '../errors/moduleNotInstalledError';
1010
import { traceError } from '../logger';
1111
import { IFileSystem } from '../platform/types';
1212
import { Architecture } from '../utils/platform';
13-
import { convertPythonVersionToSemver } from '../utils/version';
13+
import { parsePythonVersion } from '../utils/version';
1414
import { ExecutionResult, InterpreterInfomation, IProcessService, IPythonExecutionService, ObservableExecutionResult, PythonVersionInfo, SpawnOptions } from './types';
1515

1616
@injectable()
@@ -42,7 +42,7 @@ export class PythonExecutionService implements IPythonExecutionService {
4242
return {
4343
architecture: json.is64Bit ? Architecture.x64 : Architecture.x86,
4444
path: this.pythonPath,
45-
version: convertPythonVersionToSemver(versionValue),
45+
version: parsePythonVersion(versionValue),
4646
sysVersion: json.sysVersion,
4747
sysPrefix: json.sysPrefix
4848
};

src/client/common/process/types.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
import { ChildProcess, ExecOptions, SpawnOptions as ChildProcessSpawnOptions } from 'child_process';
44
import { Observable } from 'rxjs/Observable';
55
import { CancellationToken, Uri } from 'vscode';
6-
7-
import { SemVer } from 'semver';
8-
import { ExecutionInfo } from '../types';
6+
import { ExecutionInfo, Version } from '../types';
97
import { Architecture } from '../utils/platform';
108
import { EnvironmentVariables } from '../variables/types';
119

@@ -64,7 +62,7 @@ export type ReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final' | 'unknown';
6462
export type PythonVersionInfo = [number, number, number, ReleaseLevel];
6563
export type InterpreterInfomation = {
6664
path: string;
67-
version?: SemVer;
65+
version?: Version;
6866
sysVersion: string;
6967
architecture: Architecture;
7068
sysPrefix: string;

0 commit comments

Comments
 (0)