Skip to content

Commit de8e5f2

Browse files
authored
Add discovery experiment (#14868)
* Add discovery experiment * Add news item * Add experiment in more places * Tweak experiment service usage * Revert "Tweak experiment service usage" This reverts commit 68fdf5a. * Revert "Add experiment in more places" This reverts commit 5c50347. * Tweak for tests * Fix interpreter service unittest * Fix linting * Try using enable flag with experiment * Address comments. * Tweak hasInterpreters
1 parent 6d205f4 commit de8e5f2

File tree

7 files changed

+70
-24
lines changed

7 files changed

+70
-24
lines changed

news/1 Enhancements/14868.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Experiment to use the new environment discovery module.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,7 @@
10551055
"debuggerDataViewer",
10561056
"pythonSendEntireLineToREPL",
10571057
"nativeTensorBoard",
1058+
"pythonDiscoveryModule",
10581059
"All"
10591060
]
10601061
},
@@ -1083,6 +1084,7 @@
10831084
"debuggerDataViewer",
10841085
"pythonSendEntireLineToREPL",
10851086
"nativeTensorBoard",
1087+
"pythonDiscoveryModule",
10861088
"All"
10871089
]
10881090
},

src/client/common/experiments/groups.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,9 @@ export enum LinterInstallationPromptVariants {
9494
flake8First = 'pythonInstallFlake8ButtonFirst',
9595
noPrompt = 'pythonNotDisplayLinterPrompt'
9696
}
97+
98+
// Experiment to control which environment discovery mechanism can be used
99+
export enum DiscoveryVariants {
100+
discoverWithFileWatching = 'pythonDiscoveryModule',
101+
discoveryWithoutFileWatching = 'pythonDiscoveryModuleWithoutWatcher'
102+
}

src/client/interpreter/interpreterService.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export type GetInterpreterOptions = {
4444
// The parts of IComponentAdapter used here.
4545
interface IComponent {
4646
getInterpreterDetails(pythonPath: string): Promise<undefined | PythonEnvironment>;
47+
getInterpreters(resource?: Uri): Promise<PythonEnvironment[] | undefined>;
4748
}
4849

4950
@injectable()
@@ -68,7 +69,7 @@ export class InterpreterService implements Disposable, IInterpreterService {
6869
private readonly persistentStateFactory: IPersistentStateFactory;
6970
private readonly configService: IConfigurationService;
7071
private readonly interpreterPathService: IInterpreterPathService;
71-
private readonly experiments: IExperimentsManager;
72+
private readonly experimentsManager: IExperimentsManager;
7273
private readonly didChangeInterpreterEmitter = new EventEmitter<void>();
7374
private readonly didChangeInterpreterInformation = new EventEmitter<PythonEnvironment>();
7475
private readonly inMemoryCacheOfDisplayNames = new Map<string, string>();
@@ -86,7 +87,7 @@ export class InterpreterService implements Disposable, IInterpreterService {
8687
this.persistentStateFactory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory);
8788
this.configService = this.serviceContainer.get<IConfigurationService>(IConfigurationService);
8889
this.interpreterPathService = this.serviceContainer.get<IInterpreterPathService>(IInterpreterPathService);
89-
this.experiments = this.serviceContainer.get<IExperimentsManager>(IExperimentsManager);
90+
this.experimentsManager = this.serviceContainer.get<IExperimentsManager>(IExperimentsManager);
9091
}
9192

9293
public async refresh(resource?: Uri) {
@@ -105,7 +106,7 @@ export class InterpreterService implements Disposable, IInterpreterService {
105106
const workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService);
106107
const pySettings = this.configService.getSettings();
107108
this._pythonPathSetting = pySettings.pythonPath;
108-
if (this.experiments.inExperiment(DeprecatePythonPath.experiment)) {
109+
if (this.experimentsManager.inExperiment(DeprecatePythonPath.experiment)) {
109110
disposables.push(
110111
this.interpreterPathService.onDidChange((i) => {
111112
this._onConfigChanged(i.uri);
@@ -124,14 +125,22 @@ export class InterpreterService implements Disposable, IInterpreterService {
124125
});
125126
disposables.push(disposable);
126127
}
127-
this.experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control);
128+
this.experimentsManager.sendTelemetryIfInExperiment(DeprecatePythonPath.control);
128129
}
129130

130131
@captureTelemetry(EventName.PYTHON_INTERPRETER_DISCOVERY, { locator: 'all' }, true)
131132
public async getInterpreters(resource?: Uri, options?: GetInterpreterOptions): Promise<PythonEnvironment[]> {
132-
const interpreters = await this.locator.getInterpreters(resource, options);
133+
let environments: PythonEnvironment[] = [];
134+
135+
const envs = await this.pyenvs.getInterpreters(resource);
136+
if (envs !== undefined) {
137+
environments = envs;
138+
} else {
139+
environments = await this.locator.getInterpreters(resource, options);
140+
}
141+
133142
await Promise.all(
134-
interpreters
143+
environments
135144
.filter((item) => !item.displayName)
136145
.map(async (item) => {
137146
item.displayName = await this.getDisplayName(item, resource);
@@ -141,7 +150,7 @@ export class InterpreterService implements Disposable, IInterpreterService {
141150
}
142151
})
143152
);
144-
return interpreters;
153+
return environments;
145154
}
146155

147156
public dispose(): void {

src/client/pythonEnvironments/common/externalDependencies.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as fsapi from 'fs-extra';
55
import * as path from 'path';
66
import * as vscode from 'vscode';
77
import { ExecutionResult, IProcessServiceFactory, SpawnOptions } from '../../common/process/types';
8+
import { IExperimentService } from '../../common/types';
89
import { chain, iterable } from '../../common/utils/async';
910
import { getOSType, OSType } from '../../common/utils/platform';
1011
import { IDisposable } from '../../common/utils/resourceLifecycle';
@@ -114,3 +115,8 @@ export function onDidChangePythonSetting(name: string, callback: () => void): ID
114115
}
115116
});
116117
}
118+
119+
export function inExperiment(experiment: string): Promise<boolean> {
120+
const experimentService = internalServiceContainer.get<IExperimentService>(IExperimentService);
121+
return experimentService.inExperiment(experiment);
122+
}

src/client/pythonEnvironments/legacyIOC.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import { injectable } from 'inversify';
55
import * as vscode from 'vscode';
6+
import { DiscoveryVariants } from '../common/experiments/groups';
67
import { getVersionString, parseVersion } from '../common/utils/version';
78
import {
89
CONDA_ENV_FILE_SERVICE,
@@ -31,6 +32,7 @@ import { PythonEnvInfo, PythonEnvKind, PythonReleaseLevel } from './base/info';
3132
import { buildEnvInfo } from './base/info/env';
3233
import { ILocator, PythonLocatorQuery } from './base/locator';
3334
import { getEnvs } from './base/locatorUtils';
35+
import { inExperiment } from './common/externalDependencies';
3436
import { PythonInterpreterLocatorService } from './discovery/locators';
3537
import { InterpreterLocatorHelper } from './discovery/locators/helpers';
3638
import { InterpreterLocatorProgressService } from './discovery/locators/progressService';
@@ -134,19 +136,20 @@ export interface IPythonEnvironments extends ILocator {}
134136

135137
@injectable()
136138
class ComponentAdapter implements IComponentAdapter {
139+
// this will be set based on experiment
140+
private _enabled?: boolean;
141+
137142
constructor(
138143
// The adapter only wraps one thing: the component API.
139144
private readonly api: IPythonEnvironments,
140145
private readonly environmentsSecurity: IEnvironmentsSecurity,
141-
// For now we effectively disable the component.
142-
private readonly enabled = false,
143146
) {}
144147

145148
// IInterpreterHelper
146149

147150
// A result of `undefined` means "Fall back to the old code!"
148151
public async getInterpreterInformation(pythonPath: string): Promise<undefined | Partial<PythonEnvironment>> {
149-
if (!this.enabled) {
152+
if (!(await this.isEnabled())) {
150153
return undefined;
151154
}
152155
const env = await this.api.resolveEnv(pythonPath);
@@ -158,7 +161,7 @@ class ComponentAdapter implements IComponentAdapter {
158161

159162
// A result of `undefined` means "Fall back to the old code!"
160163
public async isMacDefaultPythonPath(pythonPath: string): Promise<boolean | undefined> {
161-
if (!this.enabled) {
164+
if (!(await this.isEnabled())) {
162165
return undefined;
163166
}
164167
const env = await this.api.resolveEnv(pythonPath);
@@ -177,7 +180,7 @@ class ComponentAdapter implements IComponentAdapter {
177180
pythonPath: string,
178181
resource?: vscode.Uri,
179182
): Promise<undefined | PythonEnvironment> {
180-
if (!this.enabled) {
183+
if (!(await this.isEnabled())) {
181184
return undefined;
182185
}
183186
const info = buildEnvInfo({ executable: pythonPath });
@@ -198,7 +201,7 @@ class ComponentAdapter implements IComponentAdapter {
198201

199202
// A result of `undefined` means "Fall back to the old code!"
200203
public async isCondaEnvironment(interpreterPath: string): Promise<boolean | undefined> {
201-
if (!this.enabled) {
204+
if (!(await this.isEnabled())) {
202205
return undefined;
203206
}
204207
const env = await this.api.resolveEnv(interpreterPath);
@@ -210,7 +213,7 @@ class ComponentAdapter implements IComponentAdapter {
210213

211214
// A result of `undefined` means "Fall back to the old code!"
212215
public async getCondaEnvironment(interpreterPath: string): Promise<CondaEnvironmentInfo | undefined> {
213-
if (!this.enabled) {
216+
if (!(await this.isEnabled())) {
214217
return undefined;
215218
}
216219
const env = await this.api.resolveEnv(interpreterPath);
@@ -231,25 +234,27 @@ class ComponentAdapter implements IComponentAdapter {
231234

232235
// A result of `undefined` means "Fall back to the old code!"
233236
public async isWindowsStoreInterpreter(pythonPath: string): Promise<boolean | undefined> {
234-
if (!this.enabled) {
237+
if (!(await this.isEnabled())) {
235238
return undefined;
236239
}
237240
const env = await this.api.resolveEnv(pythonPath);
238-
if (env === undefined) {
239-
return undefined;
241+
if (env) {
242+
return env.kind === PythonEnvKind.WindowsStore;
240243
}
241-
return env.kind === PythonEnvKind.WindowsStore;
244+
return undefined;
242245
}
243246

244247
// IInterpreterLocatorService
245248

246249
// A result of `undefined` means "Fall back to the old code!"
247250
public get hasInterpreters(): Promise<boolean | undefined> {
248-
if (!this.enabled) {
249-
return Promise.resolve(undefined);
250-
}
251-
const iterator = this.api.iterEnvs();
252-
return iterator.next().then((res) => !res.done);
251+
return this.isEnabled().then((enabled) => {
252+
if (enabled) {
253+
const iterator = this.api.iterEnvs();
254+
return iterator.next().then((res) => !res.done);
255+
}
256+
return undefined;
257+
});
253258
}
254259

255260
// A result of `undefined` means "Fall back to the old code!"
@@ -262,7 +267,7 @@ class ComponentAdapter implements IComponentAdapter {
262267
// onSuggestion?: boolean;
263268
// }
264269
): Promise<PythonEnvironment[] | undefined> {
265-
if (!this.enabled) {
270+
if (!(await this.isEnabled())) {
266271
return undefined;
267272
}
268273
if (options?.onSuggestion) {
@@ -285,6 +290,19 @@ class ComponentAdapter implements IComponentAdapter {
285290
const envs = await getEnvs(iterator);
286291
return envs.map(convertEnvInfo);
287292
}
293+
294+
private async isEnabled(): Promise<boolean> {
295+
if (this._enabled === undefined) {
296+
this._enabled = (await Promise.all(
297+
[
298+
inExperiment(DiscoveryVariants.discoverWithFileWatching),
299+
inExperiment(DiscoveryVariants.discoveryWithoutFileWatching),
300+
],
301+
)).includes(true);
302+
}
303+
304+
return this._enabled;
305+
}
288306
}
289307

290308
export function registerLegacyDiscoveryForIOC(

src/test/interpreters/interpreterService.unit.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { IPythonExecutionFactory, IPythonExecutionService } from '../../client/c
2121
import {
2222
IConfigurationService,
2323
IDisposableRegistry,
24+
IExperimentService,
2425
IExperimentsManager,
2526
IInterpreterPathService,
2627
InterpreterConfigurationScope,
@@ -72,6 +73,7 @@ suite('Interpreters service', () => {
7273
let configService: TypeMoq.IMock<IConfigurationService>;
7374
let interpreterPathService: TypeMoq.IMock<IInterpreterPathService>;
7475
let experimentsManager: TypeMoq.IMock<IExperimentsManager>;
76+
let experimentService: TypeMoq.IMock<IExperimentService>;
7577
let pythonSettings: TypeMoq.IMock<IPythonSettings>;
7678
let hashProviderFactory: TypeMoq.IMock<IInterpreterHashProviderFactory>;
7779

@@ -81,6 +83,7 @@ suite('Interpreters service', () => {
8183
serviceContainer = new ServiceContainer(cont);
8284

8385
experimentsManager = TypeMoq.Mock.ofType<IExperimentsManager>();
86+
experimentService = TypeMoq.Mock.ofType<IExperimentService>();
8487
interpreterPathService = TypeMoq.Mock.ofType<IInterpreterPathService>();
8588
updater = TypeMoq.Mock.ofType<IPythonPathUpdaterServiceManager>();
8689
pyenvs = TypeMoq.Mock.ofType<IComponentAdapter>();
@@ -130,6 +133,7 @@ suite('Interpreters service', () => {
130133
);
131134
serviceManager.addSingletonInstance<IFileSystem>(IFileSystem, fileSystem.object);
132135
serviceManager.addSingletonInstance<IExperimentsManager>(IExperimentsManager, experimentsManager.object);
136+
serviceManager.addSingletonInstance<IExperimentService>(IExperimentService, experimentService.object);
133137
serviceManager.addSingletonInstance<IInterpreterPathService>(
134138
IInterpreterPathService,
135139
interpreterPathService.object

0 commit comments

Comments
 (0)