Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Change the way auto-selection works #16644

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 17 commits into from
Jul 14, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
69 changes: 64 additions & 5 deletions src/client/interpreter/autoSelection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
import { inject, injectable, named } from 'inversify';
import { Event, EventEmitter, Uri } from 'vscode';
import { IWorkspaceService } from '../../common/application/types';
import { EnvironmentSorting } from '../../common/experiments/groups';
import '../../common/extensions';
import { IFileSystem } from '../../common/platform/types';
import { IPersistentState, IPersistentStateFactory, Resource } from '../../common/types';
import { IExperimentService, IPersistentState, IPersistentStateFactory, Resource } from '../../common/types';
import { createDeferred, Deferred } from '../../common/utils/async';
import { compareSemVerLikeVersions } from '../../pythonEnvironments/base/info/pythonVersion';
import { PythonEnvironment } from '../../pythonEnvironments/info';
import { captureTelemetry, sendTelemetryEvent } from '../../telemetry';
import { EventName } from '../../telemetry/constants';
import { IInterpreterHelper } from '../contracts';
import { EnvTypeHeuristic, getEnvTypeHeuristic } from '../configuration/environmentTypeComparer';
import { InterpreterComparisonType, IInterpreterComparer } from '../configuration/types';
import { IInterpreterHelper, IInterpreterService } from '../contracts';
import {
AutoSelectionRule,
IInterpreterAutoSelectionRule,
Expand Down Expand Up @@ -46,6 +49,11 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio
@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService,
@inject(IPersistentStateFactory) private readonly stateFactory: IPersistentStateFactory,
@inject(IFileSystem) private readonly fs: IFileSystem,
@inject(IExperimentService) private readonly experimentService: IExperimentService,
@inject(IInterpreterService) private readonly interpreterService: IInterpreterService,
@inject(IInterpreterComparer)
@named(InterpreterComparisonType.EnvType)
private readonly envTypeComparer: IInterpreterComparer,
@inject(IInterpreterAutoSelectionRule)
@named(AutoSelectionRule.systemWide)
systemInterpreter: IInterpreterAutoSelectionRule,
Expand Down Expand Up @@ -104,19 +112,30 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio
winRegInterpreter.setNextRule(systemInterpreter);
}

/**
* If there's a cached auto-selected interpreter -> return it.
* If not, check if we are in the env sorting experiment, and use the appropriate auto-selection logic.
*/
@captureTelemetry(EventName.PYTHON_INTERPRETER_AUTO_SELECTION, { rule: AutoSelectionRule.all }, true)
public async autoSelectInterpreter(resource: Resource): Promise<void> {
const key = this.getWorkspacePathKey(resource);

if (!this.autoSelectedWorkspacePromises.has(key)) {
const deferred = createDeferred<void>();
this.autoSelectedWorkspacePromises.set(key, deferred);

await this.initializeStore(resource);
await this.clearWorkspaceStoreIfInvalid(resource);
await this.userDefinedInterpreter.autoSelectInterpreter(resource, this);
this.didAutoSelectedInterpreterEmitter.fire();
Promise.all(this.rules.map((item) => item.autoSelectInterpreter(resource))).ignoreErrors();

if (await this.experimentService.inExperiment(EnvironmentSorting.experiment)) {
await this.autoselectInterpreterWithLocators(resource);
} else {
await this.autoselectInterpreterWithRules(resource);
}

deferred.resolve();
}

return this.autoSelectedWorkspacePromises.get(key)!.promise;
}

Expand Down Expand Up @@ -221,4 +240,44 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio
}
return undefined;
}

private async autoselectInterpreterWithRules(resource: Resource): Promise<void> {
await this.userDefinedInterpreter.autoSelectInterpreter(resource, this);

this.didAutoSelectedInterpreterEmitter.fire();

Promise.all(this.rules.map((item) => item.autoSelectInterpreter(resource))).ignoreErrors();
}

/**
* Auto-selection logic:
* 1. If there are cached interpreters (not the first session in this workspace)
* -> sort using the same logic as in the interpreter quickpick and return the first one;
* 2. If not, we already fire all the locators, so wait for their response, sort the interpreters and return the first one.
*
* `getInterpreters` will check the cache first and return early if there are any cached interpreters,
* and if not it will wait for locators to return.
* As such, we can sort interpreters based on what it returns.
*/
private async autoselectInterpreterWithLocators(resource: Resource): Promise<void> {
const interpreters = await this.interpreterService.getInterpreters(resource);
const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource);

// When auto-selecting an intepreter for a workspace, we either want to return a local one
// or fallback on a globally-installed interpreter, and we don't want want to suggest a global environment
// because we would have to add a way to match environments to a workspace.
const filteredInterpreters = interpreters.filter(
(i) => getEnvTypeHeuristic(i, workspaceUri?.folderUri.fsPath || '') !== EnvTypeHeuristic.Global,
);

filteredInterpreters.sort(this.envTypeComparer.compare.bind(this.envTypeComparer));

if (workspaceUri) {
this.setWorkspaceInterpreter(workspaceUri.folderUri, filteredInterpreters[0]);
} else {
this.setGlobalInterpreter(filteredInterpreters[0]);
}

this.didAutoSelectedInterpreterEmitter.fire();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { IInterpreterComparer } from './types';
* - Global environments (pipenv, conda);
* - Globally-installed interpreters (/usr/bin/python3, Windows Store).
*/
enum EnvTypeHeuristic {
export enum EnvTypeHeuristic {
Local = 1,
Global = 2,
GlobalInterpreters = 3,
Expand Down Expand Up @@ -165,7 +165,7 @@ function compareEnvironmentType(a: PythonEnvironment, b: PythonEnvironment, work
/**
* Return a heuristic value depending on the environment type.
*/
function getEnvTypeHeuristic(environment: PythonEnvironment, workspacePath: string): EnvTypeHeuristic {
export function getEnvTypeHeuristic(environment: PythonEnvironment, workspacePath: string): EnvTypeHeuristic {
const { envType } = environment;

if (workspacePath.length > 0 && environment.envPath && isParentPath(environment.envPath, workspacePath)) {
Expand Down
67 changes: 66 additions & 1 deletion src/test/configuration/environmentTypeComparer.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
import * as assert from 'assert';
import * as path from 'path';
import * as sinon from 'sinon';
import { EnvironmentTypeComparer } from '../../client/interpreter/configuration/environmentTypeComparer';
import {
EnvironmentTypeComparer,
EnvTypeHeuristic,
getEnvTypeHeuristic,
} from '../../client/interpreter/configuration/environmentTypeComparer';
import { IInterpreterHelper } from '../../client/interpreter/contracts';
import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info';

Expand Down Expand Up @@ -225,3 +229,64 @@ suite('Environment sorting', () => {
});
});
});

suite('getEnvTypeHeuristic tests', () => {
const workspacePath = path.join('path', 'to', 'workspace');

const localGlobalEnvTypes = [
EnvironmentType.Venv,
EnvironmentType.Conda,
EnvironmentType.VirtualEnv,
EnvironmentType.VirtualEnvWrapper,
EnvironmentType.Pipenv,
EnvironmentType.Poetry,
];

localGlobalEnvTypes.forEach((envType) => {
test('If the path to an environment starts with the workspace path it should be marked as local', () => {
const environment = {
envType,
envPath: path.join(workspacePath, 'my-environment'),
version: { major: 3, minor: 10, patch: 2 },
} as PythonEnvironment;

const envTypeHeuristic = getEnvTypeHeuristic(environment, workspacePath);

assert.strictEqual(envTypeHeuristic, EnvTypeHeuristic.Local);
});

test('If the path to an environment does not start with the workspace path it should be marked as global', () => {
const environment = {
envType,
envPath: path.join('path', 'to', 'my-environment'),
version: { major: 3, minor: 10, patch: 2 },
} as PythonEnvironment;

const envTypeHeuristic = getEnvTypeHeuristic(environment, workspacePath);

assert.strictEqual(envTypeHeuristic, EnvTypeHeuristic.Global);
});
});

const globalInterpretersEnvTypes = [
EnvironmentType.System,
EnvironmentType.WindowsStore,
EnvironmentType.Global,
EnvironmentType.Unknown,
EnvironmentType.Pyenv,
];

globalInterpretersEnvTypes.forEach((envType) => {
test(`If the environment type is ${envType} and the environment path does not start with the workspace path it should be marked as a global interpreter`, () => {
const environment = {
envType,
envPath: path.join('path', 'to', 'a', 'global', 'interpreter'),
version: { major: 3, minor: 10, patch: 2 },
} as PythonEnvironment;

const envTypeHeuristic = getEnvTypeHeuristic(environment, workspacePath);

assert.strictEqual(envTypeHeuristic, EnvTypeHeuristic.GlobalInterpreters);
});
});
});
Loading