Skip to content

Commit c46c819

Browse files
author
Kartik Raj
authored
Add an API to report progress of environment discovery in two phases (#19125)
* Add progress API * Remove old API in favor of this * Revert "Remove old API in favor of this" This reverts commit 9543501. * Revert "Revert "Remove old API in favor of this"" This reverts commit 5bbea78. * 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 * Add clarifying comment
1 parent bf68773 commit c46c819

23 files changed

+538
-231
lines changed

news/1 Enhancements/19103.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added a proposed API to report progress of environment discovery in two phases.

src/client/apiTypes.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Event, Uri } from 'vscode';
55
import { Resource } from './common/types';
66
import { IDataViewerDataProvider, IJupyterUriProvider } from './jupyter/types';
77
import { EnvPathType, PythonEnvKind } from './pythonEnvironments/base/info';
8+
import { GetRefreshEnvironmentsOptions, ProgressNotificationEvent } from './pythonEnvironments/base/locator';
89

910
/*
1011
* Do not introduce any breaking changes to this API.
@@ -203,11 +204,17 @@ export interface IProposedExtensionAPI {
203204
* is triggered.
204205
*/
205206
refreshEnvironment(options?: RefreshEnvironmentsOptions): Promise<EnvPathType[] | undefined>;
207+
/**
208+
* Tracks discovery progress for current list of known environments, i.e when it starts, finishes or any other relevant
209+
* stage. Note the progress for a particular query is currently not tracked or reported, this only indicates progress of
210+
* the entire collection.
211+
*/
212+
readonly onRefreshProgress: Event<ProgressNotificationEvent>;
206213
/**
207214
* Returns a promise for the ongoing refresh. Returns `undefined` if there are no active
208215
* refreshes going on.
209216
*/
210-
getRefreshPromise(): Promise<void> | undefined;
217+
getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise<void> | undefined;
211218
/**
212219
* This event is triggered when the known environment list changes, like when a environment
213220
* is found, existing environment is removed, or some details changed on an environment.

src/client/interpreter/contracts.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ 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 { PythonLocatorQuery } from '../pythonEnvironments/base/locator';
6+
import { ProgressNotificationEvent, PythonLocatorQuery } from '../pythonEnvironments/base/locator';
77
import { CondaEnvironmentInfo } from '../pythonEnvironments/common/environmentManagers/conda';
88
import { EnvironmentType, PythonEnvironment } from '../pythonEnvironments/info';
99

@@ -16,9 +16,9 @@ export type PythonEnvironmentsChangedEvent = {
1616

1717
export const IComponentAdapter = Symbol('IComponentAdapter');
1818
export interface IComponentAdapter {
19-
readonly onRefreshStart: Event<void>;
19+
readonly onProgress: Event<ProgressNotificationEvent>;
2020
triggerRefresh(query?: PythonLocatorQuery & { clearCache?: boolean }, trigger?: 'auto' | 'ui'): Promise<void>;
21-
readonly refreshPromise: Promise<void> | undefined;
21+
getRefreshPromise(): Promise<void> | undefined;
2222
readonly onChanged: Event<PythonEnvironmentsChangedEvent>;
2323
// VirtualEnvPrompt
2424
onDidCreate(resource: Resource, callback: () => void): Disposable;
@@ -63,7 +63,6 @@ export interface ICondaService {
6363

6464
export const IInterpreterService = Symbol('IInterpreterService');
6565
export interface IInterpreterService {
66-
readonly onRefreshStart: Event<void>;
6766
triggerRefresh(query?: PythonLocatorQuery & { clearCache?: boolean }, trigger?: 'auto' | 'ui'): Promise<void>;
6867
readonly refreshPromise: Promise<void> | undefined;
6968
readonly onDidChangeInterpreters: Event<PythonEnvironmentsChangedEvent>;

src/client/interpreter/display/progressDisplay.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { IDisposableRegistry } from '../../common/types';
1212
import { createDeferred, Deferred } from '../../common/utils/async';
1313
import { Interpreters } from '../../common/utils/localize';
1414
import { traceDecoratorVerbose } from '../../logging';
15+
import { ProgressReportStage } from '../../pythonEnvironments/base/locator';
1516
import { IComponentAdapter } from '../contracts';
1617

1718
// The parts of IComponentAdapter used here.
@@ -30,11 +31,14 @@ export class InterpreterLocatorProgressStatubarHandler implements IExtensionSing
3031
) {}
3132

3233
public async activate(): Promise<void> {
33-
this.pyenvs.onRefreshStart(
34-
() => {
35-
this.showProgress();
36-
if (this.pyenvs.refreshPromise) {
37-
this.pyenvs.refreshPromise.then(() => this.hideProgress());
34+
this.pyenvs.onProgress(
35+
(event) => {
36+
if (event.stage === ProgressReportStage.discoveryStarted) {
37+
this.showProgress();
38+
const refreshPromise = this.pyenvs.getRefreshPromise();
39+
if (refreshPromise) {
40+
refreshPromise.then(() => this.hideProgress());
41+
}
3842
}
3943
},
4044
this,

src/client/interpreter/interpreterService.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,6 @@ export class InterpreterService implements Disposable, IInterpreterService {
4040
return this.pyenvs.hasInterpreters(filter);
4141
}
4242

43-
public get onRefreshStart(): Event<void> {
44-
return this.pyenvs.onRefreshStart;
45-
}
46-
4743
public triggerRefresh(
4844
query?: PythonLocatorQuery & { clearCache?: boolean },
4945
trigger?: 'auto' | 'ui',
@@ -52,7 +48,7 @@ export class InterpreterService implements Disposable, IInterpreterService {
5248
}
5349

5450
public get refreshPromise(): Promise<void> | undefined {
55-
return this.pyenvs.refreshPromise;
51+
return this.pyenvs.getRefreshPromise();
5652
}
5753

5854
public get onDidChangeInterpreter(): Event<void> {

src/client/proposedApi.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { IInterpreterService } from './interpreter/contracts';
1616
import { IServiceContainer } from './ioc/types';
1717
import { PythonEnvInfo } from './pythonEnvironments/base/info';
1818
import { getEnvPath } from './pythonEnvironments/base/info/env';
19-
import { IDiscoveryAPI } from './pythonEnvironments/base/locator';
19+
import { GetRefreshEnvironmentsOptions, IDiscoveryAPI } from './pythonEnvironments/base/locator';
2020

2121
const onDidInterpretersChangedEvent = new EventEmitter<EnvironmentsChangedParams[]>();
2222
export function reportInterpretersChanged(e: EnvironmentsChangedParams[]): void {
@@ -106,12 +106,13 @@ export function buildProposedApi(
106106
const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location));
107107
return Promise.resolve(paths);
108108
},
109-
getRefreshPromise(): Promise<void> | undefined {
110-
return discoveryApi.refreshPromise;
109+
getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise<void> | undefined {
110+
return discoveryApi.getRefreshPromise(options);
111111
},
112112
onDidChangeExecutionDetails: interpreterService.onDidChangeInterpreterConfiguration,
113113
onDidEnvironmentsChanged: onDidInterpretersChangedEvent.event,
114114
onDidActiveEnvironmentChanged: onDidActiveInterpreterChangedEvent.event,
115+
onRefreshProgress: discoveryApi.onProgress,
115116
},
116117
};
117118
return proposed;

src/client/pythonEnvironments/api.ts

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

4+
import { Event } from 'vscode';
45
import { StopWatch } from '../common/utils/stopWatch';
56
import { sendTelemetryEvent } from '../telemetry';
67
import { EventName } from '../telemetry/constants';
78
import { getEnvPath } from './base/info/env';
8-
import { IDiscoveryAPI, PythonLocatorQuery } from './base/locator';
9+
import {
10+
GetRefreshEnvironmentsOptions,
11+
IDiscoveryAPI,
12+
ProgressNotificationEvent,
13+
PythonLocatorQuery,
14+
} from './base/locator';
915

1016
export type GetLocatorFunc = () => Promise<IDiscoveryAPI>;
1117

@@ -26,12 +32,12 @@ class PythonEnvironments implements IDiscoveryAPI {
2632
this.locator = await this.getLocator();
2733
}
2834

29-
public get onRefreshStart() {
30-
return this.locator.onRefreshStart;
35+
public get onProgress(): Event<ProgressNotificationEvent> {
36+
return this.locator.onProgress;
3137
}
3238

33-
public get refreshPromise() {
34-
return this.locator.refreshPromise;
39+
public getRefreshPromise(options?: GetRefreshEnvironmentsOptions) {
40+
return this.locator.getRefreshPromise(options);
3541
}
3642

3743
public get onChanged() {

src/client/pythonEnvironments/base/locator.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,23 @@ export interface IPythonEnvsIterator<I = PythonEnvInfo> extends IAsyncIterableIt
6464
* If this property is not provided then it means the iterator does
6565
* not support updates.
6666
*/
67-
onUpdated?: Event<PythonEnvUpdatedEvent<I> | null>;
67+
onUpdated?: Event<PythonEnvUpdatedEvent<I> | ProgressNotificationEvent>;
68+
}
69+
70+
export enum ProgressReportStage {
71+
discoveryStarted = 'discoveryStarted',
72+
allPathsDiscovered = 'allPathsDiscovered',
73+
discoveryFinished = 'discoveryFinished',
74+
}
75+
76+
export type ProgressNotificationEvent = {
77+
stage: ProgressReportStage;
78+
};
79+
80+
export function isProgressEvent<I = PythonEnvInfo>(
81+
event: PythonEnvUpdatedEvent<I> | ProgressNotificationEvent,
82+
): event is ProgressNotificationEvent {
83+
return 'stage' in event;
6884
}
6985

7086
/**
@@ -170,11 +186,20 @@ interface IResolver {
170186

171187
export interface IResolvingLocator<I = PythonEnvInfo> extends IResolver, ILocator<I> {}
172188

189+
export interface GetRefreshEnvironmentsOptions {
190+
/**
191+
* Get refresh promise which resolves once the following stage has been reached for the list of known environments.
192+
*/
193+
stage?: ProgressReportStage;
194+
}
195+
173196
export interface IDiscoveryAPI {
174197
/**
175-
* Fires when the known list of environments starts refreshing, i.e when discovery starts or restarts.
198+
* Tracks discovery progress for current list of known environments, i.e when it starts, finishes or any other relevant
199+
* stage. Note the progress for a particular query is currently not tracked or reported, this only indicates progress of
200+
* the entire collection.
176201
*/
177-
readonly onRefreshStart: Event<void>;
202+
readonly onProgress: Event<ProgressNotificationEvent>;
178203
/**
179204
* Fires with details if the known list changes.
180205
*/
@@ -183,7 +208,7 @@ export interface IDiscoveryAPI {
183208
* Resolves once environment list has finished refreshing, i.e all environments are
184209
* discovered. Carries `undefined` if there is no refresh currently going on.
185210
*/
186-
readonly refreshPromise: Promise<void> | undefined;
211+
getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise<void> | undefined;
187212
/**
188213
* Triggers a new refresh for query if there isn't any already running.
189214
*/

src/client/pythonEnvironments/base/locatorUtils.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@ import { createDeferred } from '../../common/utils/async';
66
import { getURIFilter } from '../../common/utils/misc';
77
import { traceVerbose } from '../../logging';
88
import { PythonEnvInfo } from './info';
9-
import { IPythonEnvsIterator, PythonEnvUpdatedEvent, PythonLocatorQuery } from './locator';
9+
import {
10+
IPythonEnvsIterator,
11+
isProgressEvent,
12+
ProgressNotificationEvent,
13+
ProgressReportStage,
14+
PythonEnvUpdatedEvent,
15+
PythonLocatorQuery,
16+
} from './locator';
1017

1118
/**
1219
* Create a filter function to match the given query.
@@ -71,8 +78,11 @@ export async function getEnvs<I = PythonEnvInfo>(iterator: IPythonEnvsIterator<I
7178
if (iterator.onUpdated === undefined) {
7279
updatesDone.resolve();
7380
} else {
74-
const listener = iterator.onUpdated((event: PythonEnvUpdatedEvent<I> | null) => {
75-
if (event === null) {
81+
const listener = iterator.onUpdated((event: PythonEnvUpdatedEvent<I> | ProgressNotificationEvent) => {
82+
if (isProgressEvent(event)) {
83+
if (event.stage !== ProgressReportStage.discoveryFinished) {
84+
return;
85+
}
7686
updatesDone.resolve();
7787
listener.dispose();
7888
} else {

src/client/pythonEnvironments/base/locators.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@
44
import { chain } from '../../common/utils/async';
55
import { Disposables } from '../../common/utils/resourceLifecycle';
66
import { PythonEnvInfo } from './info';
7-
import { ILocator, IPythonEnvsIterator, PythonEnvUpdatedEvent, PythonLocatorQuery } from './locator';
7+
import {
8+
ILocator,
9+
IPythonEnvsIterator,
10+
isProgressEvent,
11+
ProgressNotificationEvent,
12+
ProgressReportStage,
13+
PythonEnvUpdatedEvent,
14+
PythonLocatorQuery,
15+
} from './locator';
816
import { PythonEnvsWatchers } from './watchers';
917

1018
/**
@@ -19,17 +27,21 @@ export function combineIterators<I>(iterators: IPythonEnvsIterator<I>[]): IPytho
1927
}
2028

2129
// eslint-disable-next-line @typescript-eslint/no-explicit-any
22-
result.onUpdated = (handleEvent: (e: PythonEnvUpdatedEvent<I> | null) => any) => {
30+
result.onUpdated = (handleEvent: (e: PythonEnvUpdatedEvent<I> | ProgressNotificationEvent) => any) => {
2331
const disposables = new Disposables();
2432
let numActive = events.length;
2533
events.forEach((event) => {
26-
const disposable = event!((e: PythonEnvUpdatedEvent<I> | null) => {
34+
const disposable = event!((e: PythonEnvUpdatedEvent<I> | ProgressNotificationEvent) => {
2735
// NOSONAR
28-
if (e === null) {
29-
numActive -= 1;
30-
if (numActive === 0) {
31-
// All the sub-events are done so we're done.
32-
handleEvent(null);
36+
if (isProgressEvent(e)) {
37+
if (e.stage === ProgressReportStage.discoveryFinished) {
38+
numActive -= 1;
39+
if (numActive === 0) {
40+
// All the sub-events are done so we're done.
41+
handleEvent({ stage: ProgressReportStage.discoveryFinished });
42+
}
43+
} else {
44+
handleEvent({ stage: e.stage });
3345
}
3446
} else {
3547
handleEvent(e);

0 commit comments

Comments
 (0)