Skip to content

Commit b7a4321

Browse files
Kartik Rajkarthiknadig
authored andcommitted
Only trigger auto environment discovery if a user attempts to choose a different interpreter, or when a particular scope is opened for the first time (microsoft/vscode-python#19162)
* Add progress API * Remove old API in favor of this * Revert "Remove old API in favor of this" This reverts commit 9543501576cd063e6aad1ef3ddd7a39cb3354b22. * Revert "Revert "Remove old API in favor of this"" This reverts commit 5bbea78879f2447c6783d7ad98610d4af36abf29. * Fix * Remove old API impl * Add to proposed API * Add get refresh promise options * Change getter to function * Translate progress events to progress promises * Fix combining iterators * Fix tests * Add test for resolver * Add test for reducer * Fixes * Add tests for environment collection service * News entry * Add another test if a query is provided * Add comments and clarify * Fix bug * Use extensionUrl instead of extensionPath (microsoft/vscode-python#19122) * Only trigger auto environment discovery once in the first session for a particular scope * Oops * Ensure old Jupyter APIs still have the old behavior * Trigger auto-discovery if a user is attempting to choose a different interpreter * Fix tests * Temp * Add an option to trigger refresh for the session * Fix bugs * Fix tests * Remove trigger from telemetry * Add tests for options Co-authored-by: Karthik Nadig <[email protected]>
1 parent 4ba34bb commit b7a4321

File tree

17 files changed

+310
-216
lines changed

17 files changed

+310
-216
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Only trigger auto environment discovery if a user attempts to choose a different interpreter, or when a particular scope (a workspace folder or globally) is opened for the first time.

extensions/positron-python/src/client/interpreter/autoSelection/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,8 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio
200200
});
201201
}
202202

203-
const interpreters = await this.interpreterService.getAllInterpreters(resource);
203+
await this.interpreterService.refreshPromise;
204+
const interpreters = this.interpreterService.getInterpreters(resource);
204205
const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource);
205206

206207
const recommendedInterpreter = this.envTypeComparer.getRecommended(interpreters, workspaceUri?.folderUri);

extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand {
111111
// times so that the visible items do not change.
112112
const preserveOrderWhenFiltering = !!this.interpreterService.refreshPromise;
113113
const suggestions = this.getItems(state.workspace);
114+
// Discovery is no longer guranteed to be auto-triggered on extension load, so trigger it when
115+
// user interacts with the interpreter picker but only once per session. Users can rely on the
116+
// refresh button if they want to trigger it more than once.
117+
this.interpreterService.triggerRefresh(undefined, { ifNotTriggerredAlready: true }).ignoreErrors();
114118
state.path = undefined;
115119
const currentInterpreterPathDisplay = this.pathUtils.getDisplayName(
116120
this.configurationService.getSettings(state.workspace).pythonPath,
@@ -134,7 +138,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand {
134138
iconPath: getIcon(REFRESH_BUTTON_ICON),
135139
tooltip: InterpreterQuickPickList.refreshInterpreterList,
136140
},
137-
callback: () => this.interpreterService.triggerRefresh(undefined, 'ui').ignoreErrors(),
141+
callback: () => this.interpreterService.triggerRefresh().ignoreErrors(),
138142
},
139143
onChangeItem: {
140144
event: this.interpreterService.onDidChangeInterpreters,
@@ -375,7 +379,6 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand {
375379
if (!targetConfig) {
376380
return;
377381
}
378-
379382
const { configTarget } = targetConfig[0];
380383
const wkspace = targetConfig[0].folderUri;
381384
const interpreterState: InterpreterStateArgs = { path: undefined, workspace: wkspace };

extensions/positron-python/src/client/interpreter/configuration/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export interface IInterpreterSelector extends Disposable {
2929
suggestions: IInterpreterQuickPickItem[],
3030
resource: Resource,
3131
): IInterpreterQuickPickItem | undefined;
32+
/**
33+
* @deprecated Only exists for old Jupyter integration.
34+
*/
3235
getAllSuggestions(resource: Resource): Promise<IInterpreterQuickPickItem[]>;
3336
getSuggestions(resource: Resource, useFullDisplayName?: boolean): IInterpreterQuickPickItem[];
3437
suggestionToQuickPickItem(

extensions/positron-python/src/client/interpreter/contracts.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { CodeLensProvider, ConfigurationTarget, Disposable, Event, TextDocument,
33
import { FileChangeType } from '../common/platform/fileSystemWatcher';
44
import { Resource } from '../common/types';
55
import { PythonEnvSource } from '../pythonEnvironments/base/info';
6-
import { ProgressNotificationEvent, PythonLocatorQuery } from '../pythonEnvironments/base/locator';
6+
import {
7+
ProgressNotificationEvent,
8+
PythonLocatorQuery,
9+
TriggerRefreshOptions,
10+
} from '../pythonEnvironments/base/locator';
711
import { CondaEnvironmentInfo } from '../pythonEnvironments/common/environmentManagers/conda';
812
import { EnvironmentType, PythonEnvironment } from '../pythonEnvironments/info';
913

@@ -17,7 +21,7 @@ export type PythonEnvironmentsChangedEvent = {
1721
export const IComponentAdapter = Symbol('IComponentAdapter');
1822
export interface IComponentAdapter {
1923
readonly onProgress: Event<ProgressNotificationEvent>;
20-
triggerRefresh(query?: PythonLocatorQuery & { clearCache?: boolean }, trigger?: 'auto' | 'ui'): Promise<void>;
24+
triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise<void>;
2125
getRefreshPromise(): Promise<void> | undefined;
2226
readonly onChanged: Event<PythonEnvironmentsChangedEvent>;
2327
// VirtualEnvPrompt
@@ -63,14 +67,17 @@ export interface ICondaService {
6367

6468
export const IInterpreterService = Symbol('IInterpreterService');
6569
export interface IInterpreterService {
66-
triggerRefresh(query?: PythonLocatorQuery & { clearCache?: boolean }, trigger?: 'auto' | 'ui'): Promise<void>;
70+
triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise<void>;
6771
readonly refreshPromise: Promise<void> | undefined;
6872
readonly onDidChangeInterpreters: Event<PythonEnvironmentsChangedEvent>;
6973
onDidChangeInterpreterConfiguration: Event<Uri | undefined>;
7074
onDidChangeInterpreter: Event<void>;
7175
onDidChangeInterpreterInformation: Event<PythonEnvironment>;
7276
hasInterpreters(filter?: (e: PythonEnvironment) => Promise<boolean>): Promise<boolean>;
7377
getInterpreters(resource?: Uri): PythonEnvironment[];
78+
/**
79+
* @deprecated Only exists for old Jupyter integration.
80+
*/
7481
getAllInterpreters(resource?: Uri): Promise<PythonEnvironment[]>;
7582
getActiveInterpreter(resource?: Uri): Promise<PythonEnvironment | undefined>;
7683
getInterpreterDetails(pythonPath: string, resoure?: Uri): Promise<undefined | PythonEnvironment>;

extensions/positron-python/src/client/interpreter/interpreterService.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
IInterpreterStatusbarVisibilityFilter,
2121
PythonEnvironmentsChangedEvent,
2222
} from './contracts';
23-
import { PythonLocatorQuery } from '../pythonEnvironments/base/locator';
2423
import { traceError, traceLog } from '../logging';
2524
import { Commands, PYTHON_LANGUAGE } from '../common/constants';
2625
import { reportActiveInterpreterChanged } from '../proposedApi';
@@ -29,6 +28,7 @@ import { Interpreters } from '../common/utils/localize';
2928
import { sendTelemetryEvent } from '../telemetry';
3029
import { EventName } from '../telemetry/constants';
3130
import { cache } from '../common/utils/decorators';
31+
import { PythonLocatorQuery, TriggerRefreshOptions } from '../pythonEnvironments/base/locator';
3232

3333
type StoredPythonEnvironment = PythonEnvironment & { store?: boolean };
3434

@@ -40,11 +40,8 @@ export class InterpreterService implements Disposable, IInterpreterService {
4040
return this.pyenvs.hasInterpreters(filter);
4141
}
4242

43-
public triggerRefresh(
44-
query?: PythonLocatorQuery & { clearCache?: boolean },
45-
trigger?: 'auto' | 'ui',
46-
): Promise<void> {
47-
return this.pyenvs.triggerRefresh(query, trigger);
43+
public triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise<void> {
44+
return this.pyenvs.triggerRefresh(query, options);
4845
}
4946

5047
public get refreshPromise(): Promise<void> | undefined {
@@ -140,6 +137,10 @@ export class InterpreterService implements Disposable, IInterpreterService {
140137
}
141138

142139
public async getAllInterpreters(resource?: Uri): Promise<PythonEnvironment[]> {
140+
// For backwards compatibility with old Jupyter APIs, ensure a
141+
// fresh refresh is always triggered when using the API. As it is
142+
// no longer auto-triggered by the extension.
143+
this.triggerRefresh(undefined, { ifNotTriggerredAlready: true }).ignoreErrors();
143144
await this.refreshPromise;
144145
return this.getInterpreters(resource);
145146
}
@@ -211,7 +212,7 @@ export class InterpreterService implements Disposable, IInterpreterService {
211212
traceLog('Conda envs without Python are known to not work well; fixing conda environment...');
212213
const promise = installer.install(Product.python, await this.getInterpreterDetails(pythonPath));
213214
shell.withProgress(progressOptions, () => promise);
214-
promise.then(() => this.triggerRefresh({ clearCache: true }).ignoreErrors());
215+
promise.then(() => this.triggerRefresh(undefined, { clearCache: true }).ignoreErrors());
215216
}
216217
}
217218
}

extensions/positron-python/src/client/proposedApi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export function buildProposedApi(
102102
return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path);
103103
},
104104
async refreshEnvironment(options?: RefreshEnvironmentsOptions) {
105-
await discoveryApi.triggerRefresh(options ? { clearCache: options.clearCache } : undefined);
105+
await discoveryApi.triggerRefresh(undefined, options ? { clearCache: options.clearCache } : undefined);
106106
const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location));
107107
return Promise.resolve(paths);
108108
},

extensions/positron-python/src/client/pythonEnvironments/api.ts

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,12 @@
22
// Licensed under the MIT License.
33

44
import { Event } from 'vscode';
5-
import { StopWatch } from '../common/utils/stopWatch';
6-
import { sendTelemetryEvent } from '../telemetry';
7-
import { EventName } from '../telemetry/constants';
8-
import { getEnvPath } from './base/info/env';
95
import {
106
GetRefreshEnvironmentsOptions,
117
IDiscoveryAPI,
128
ProgressNotificationEvent,
139
PythonLocatorQuery,
10+
TriggerRefreshOptions,
1411
} from './base/locator';
1512

1613
export type GetLocatorFunc = () => Promise<IDiscoveryAPI>;
@@ -52,20 +49,8 @@ class PythonEnvironments implements IDiscoveryAPI {
5249
return this.locator.resolveEnv(env);
5350
}
5451

55-
public async triggerRefresh(query?: PythonLocatorQuery, trigger?: 'auto' | 'ui') {
56-
const stopWatch = new StopWatch();
57-
await this.locator.triggerRefresh(query);
58-
if (!query) {
59-
// Intent is to capture time taken for all of discovery to complete, so make sure
60-
// all interpreters are queried for.
61-
sendTelemetryEvent(EventName.PYTHON_INTERPRETER_DISCOVERY, stopWatch.elapsedTime, {
62-
interpreters: this.getEnvs().length,
63-
environmentsWithoutPython: this.getEnvs().filter(
64-
(e) => getEnvPath(e.executable.filename, e.location).pathType === 'envFolderPath',
65-
).length,
66-
trigger: trigger ?? 'auto',
67-
});
68-
}
52+
public async triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions) {
53+
return this.locator.triggerRefresh(query, options);
6954
}
7055
}
7156

extensions/positron-python/src/client/pythonEnvironments/base/locator.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,17 @@ export interface GetRefreshEnvironmentsOptions {
193193
stage?: ProgressReportStage;
194194
}
195195

196+
export type TriggerRefreshOptions = {
197+
/**
198+
* Trigger a fresh refresh.
199+
*/
200+
clearCache?: boolean;
201+
/**
202+
* Only trigger a refresh if it hasn't already been triggered for this session.
203+
*/
204+
ifNotTriggerredAlready?: boolean;
205+
};
206+
196207
export interface IDiscoveryAPI {
197208
/**
198209
* Tracks discovery progress for current list of known environments, i.e when it starts, finishes or any other relevant
@@ -212,7 +223,7 @@ export interface IDiscoveryAPI {
212223
/**
213224
* Triggers a new refresh for query if there isn't any already running.
214225
*/
215-
triggerRefresh(query?: PythonLocatorQuery & { clearCache?: boolean }, trigger?: 'auto' | 'ui'): Promise<void>;
226+
triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise<void>;
216227
/**
217228
* Get current list of known environments.
218229
*/

extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
import { Event, EventEmitter } from 'vscode';
55
import '../../../../common/extensions';
66
import { createDeferred, Deferred } from '../../../../common/utils/async';
7+
import { StopWatch } from '../../../../common/utils/stopWatch';
78
import { traceError } from '../../../../logging';
9+
import { sendTelemetryEvent } from '../../../../telemetry';
10+
import { EventName } from '../../../../telemetry/constants';
811
import { normalizePath } from '../../../common/externalDependencies';
912
import { PythonEnvInfo } from '../../info';
13+
import { getEnvPath } from '../../info/env';
1014
import {
1115
GetRefreshEnvironmentsOptions,
1216
IDiscoveryAPI,
@@ -15,6 +19,7 @@ import {
1519
ProgressNotificationEvent,
1620
ProgressReportStage,
1721
PythonLocatorQuery,
22+
TriggerRefreshOptions,
1823
} from '../../locator';
1924
import { getQueryFilter } from '../../locatorUtils';
2025
import { PythonEnvCollectionChangedEvent, PythonEnvsWatcher } from '../../watcher';
@@ -33,6 +38,9 @@ export class EnvsCollectionService extends PythonEnvsWatcher<PythonEnvCollection
3338
/** Keeps track of promises which resolves when a stage has been reached */
3439
private progressPromises = new Map<ProgressReportStage, Deferred<void>>();
3540

41+
/** Keeps track of whether a refresh has been triggered for various queries. */
42+
private wasRefreshTriggeredForQuery = new Map<PythonLocatorQuery | undefined, boolean>();
43+
3644
private readonly progress = new EventEmitter<ProgressNotificationEvent>();
3745

3846
public get onProgress(): Event<ProgressNotificationEvent> {
@@ -89,24 +97,25 @@ export class EnvsCollectionService extends PythonEnvsWatcher<PythonEnvCollection
8997

9098
public getEnvs(query?: PythonLocatorQuery): PythonEnvInfo[] {
9199
const cachedEnvs = this.cache.getAllEnvs();
92-
if (cachedEnvs.length === 0 && this.refreshesPerQuery.size === 0) {
93-
// We expect a refresh to already be triggered when activating discovery component.
94-
traceError('No python is installed or a refresh has not already been triggered');
95-
this.triggerRefresh().ignoreErrors();
96-
}
97100
return query ? cachedEnvs.filter(getQueryFilter(query)) : cachedEnvs;
98101
}
99102

100-
public triggerRefresh(query?: PythonLocatorQuery & { clearCache?: boolean }): Promise<void> {
103+
public triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise<void> {
104+
const stopWatch = new StopWatch();
105+
if (options?.ifNotTriggerredAlready) {
106+
if (this.wasRefreshTriggered(query)) {
107+
return Promise.resolve(); // Refresh was already triggered, return.
108+
}
109+
}
101110
let refreshPromise = this.getRefreshPromiseForQuery(query);
102111
if (!refreshPromise) {
103-
refreshPromise = this.startRefresh(query);
112+
refreshPromise = this.startRefresh(query, options);
104113
}
105-
return refreshPromise;
114+
return refreshPromise.then(() => this.sendTelemetry(query, stopWatch));
106115
}
107116

108-
private startRefresh(query: (PythonLocatorQuery & { clearCache?: boolean }) | undefined): Promise<void> {
109-
if (query?.clearCache) {
117+
private startRefresh(query: PythonLocatorQuery | undefined, options?: TriggerRefreshOptions): Promise<void> {
118+
if (options?.clearCache) {
110119
this.cache.clearCache();
111120
}
112121
this.createProgressStates(query);
@@ -182,6 +191,10 @@ export class EnvsCollectionService extends PythonEnvsWatcher<PythonEnvCollection
182191
return this.refreshesPerQuery.get(query)?.promise ?? this.refreshesPerQuery.get(undefined)?.promise;
183192
}
184193

194+
private wasRefreshTriggered(query?: PythonLocatorQuery) {
195+
return this.wasRefreshTriggeredForQuery.get(query) ?? this.wasRefreshTriggeredForQuery.get(undefined);
196+
}
197+
185198
/**
186199
* Ensure we trigger a fresh refresh for the query after the current refresh (if any) is done.
187200
*/
@@ -203,6 +216,7 @@ export class EnvsCollectionService extends PythonEnvsWatcher<PythonEnvCollection
203216

204217
private createProgressStates(query: PythonLocatorQuery | undefined) {
205218
this.refreshesPerQuery.set(query, createDeferred<void>());
219+
this.wasRefreshTriggeredForQuery.set(query, true);
206220
Object.values(ProgressReportStage).forEach((stage) => {
207221
this.progressPromises.set(stage, createDeferred<void>());
208222
});
@@ -230,4 +244,16 @@ export class EnvsCollectionService extends PythonEnvsWatcher<PythonEnvCollection
230244
this.progress.fire({ stage: ProgressReportStage.discoveryFinished });
231245
}
232246
}
247+
248+
private sendTelemetry(query: PythonLocatorQuery | undefined, stopWatch: StopWatch) {
249+
if (!query && !this.wasRefreshTriggered(query)) {
250+
// Intent is to capture time taken for discovery of all envs to complete the first time.
251+
sendTelemetryEvent(EventName.PYTHON_INTERPRETER_DISCOVERY, stopWatch.elapsedTime, {
252+
interpreters: this.cache.getAllEnvs().length,
253+
environmentsWithoutPython: this.cache
254+
.getAllEnvs()
255+
.filter((e) => getEnvPath(e.executable.filename, e.location).pathType === 'envFolderPath').length,
256+
});
257+
}
258+
}
233259
}

extensions/positron-python/src/client/pythonEnvironments/index.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import { PythonEnvsReducer } from './base/locators/composite/envsReducer';
1212
import { PythonEnvsResolver } from './base/locators/composite/envsResolver';
1313
import { WindowsPathEnvVarLocator } from './base/locators/lowLevel/windowsKnownPathsLocator';
1414
import { WorkspaceVirtualEnvironmentLocator } from './base/locators/lowLevel/workspaceVirtualEnvLocator';
15-
import { initializeExternalDependencies as initializeLegacyExternalDependencies } from './common/externalDependencies';
15+
import {
16+
initializeExternalDependencies as initializeLegacyExternalDependencies,
17+
normCasePath,
18+
} from './common/externalDependencies';
1619
import { ExtensionLocators, WatchRootsArgs, WorkspaceLocators } from './base/locators/wrappers';
1720
import { CustomVirtualEnvironmentLocator } from './base/locators/lowLevel/customVirtualEnvLocator';
1821
import { CondaEnvironmentLocator } from './base/locators/lowLevel/condaLocator';
@@ -52,20 +55,44 @@ export async function initialize(ext: ExtensionState): Promise<IDiscoveryAPI> {
5255
/**
5356
* Make use of the component (e.g. register with VS Code).
5457
*/
55-
export async function activate(api: IDiscoveryAPI, _ext: ExtensionState): Promise<ActivationResult> {
58+
export async function activate(api: IDiscoveryAPI, ext: ExtensionState): Promise<ActivationResult> {
5659
/**
5760
* Force an initial background refresh of the environments.
5861
*
59-
* Note API is ready to be queried only after a refresh has been triggered, and extension activation is blocked on API. So,
60-
* * If discovery was never triggered, we need to block extension activation on the refresh trigger.
61-
* * If discovery was already triggered, it maybe the case that this is a new workspace for which it hasn't been triggered yet.
62-
* So always trigger discovery as part of extension activation for now.
63-
*
64-
* TODO: https://github.com/microsoft/vscode-python/issues/17498
65-
* Once `onInterpretersChanged` event is exposed via API, we can probably expect extensions to rely on that and
66-
* discovery can be triggered after activation, especially in the second case.
62+
* Note API is ready to be queried only after a refresh has been triggered, and extension activation is
63+
* blocked on API being ready. So if discovery was never triggered for a scope, we need to block
64+
* extension activation on the "refresh trigger".
6765
*/
68-
api.triggerRefresh().ignoreErrors();
66+
const folders = vscode.workspace.workspaceFolders;
67+
const wasTriggered = getGlobalStorage<boolean>(ext.context, 'PYTHON_WAS_DISCOVERY_TRIGGERED', false);
68+
if (!wasTriggered.get()) {
69+
api.triggerRefresh().ignoreErrors();
70+
wasTriggered.set(true).then(() => {
71+
folders?.forEach(async (folder) => {
72+
const wasTriggeredForFolder = getGlobalStorage<boolean>(
73+
ext.context,
74+
`PYTHON_WAS_DISCOVERY_TRIGGERED_${normCasePath(folder.uri.fsPath)}`,
75+
false,
76+
);
77+
await wasTriggeredForFolder.set(true);
78+
});
79+
});
80+
} else {
81+
// Figure out which workspace folders need to be activated.
82+
folders?.forEach(async (folder) => {
83+
const wasTriggeredForFolder = getGlobalStorage<boolean>(
84+
ext.context,
85+
`PYTHON_WAS_DISCOVERY_TRIGGERED_${normCasePath(folder.uri.fsPath)}`,
86+
false,
87+
);
88+
if (!wasTriggeredForFolder.get()) {
89+
api.triggerRefresh({
90+
searchLocations: { roots: [folder.uri], doNotIncludeNonRooted: true },
91+
}).ignoreErrors();
92+
await wasTriggeredForFolder.set(true);
93+
}
94+
});
95+
}
6996

7097
return {
7198
fullyReady: Promise.resolve(),

0 commit comments

Comments
 (0)