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

Skip to content

Commit 5f09a26

Browse files
author
Kartik Raj
authored
Added environments reducer (#13953)
* Add environments reducer * Added tests * Use path.join to construct paths * Code reviews * Correct dummy implementations and adjust tests * Modify resolveEnv() * Rename to a general parentLocator
1 parent f60eeaf commit 5f09a26

File tree

5 files changed

+425
-6
lines changed

5 files changed

+425
-6
lines changed

src/client/pythonEnvironments/base/info/index.ts

+29
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { Uri } from 'vscode';
55
import { Architecture } from '../../../common/utils/platform';
66
import { BasicVersionInfo, VersionInfo } from '../../../common/utils/version';
7+
import { arePathsSame } from '../../common/externalDependencies';
78

89
/**
910
* IDs for the various supported Python environments.
@@ -143,3 +144,31 @@ export type PythonEnvInfo = _PythonEnvInfo & {
143144
defaultDisplayName?: string;
144145
searchLocation?: Uri;
145146
};
147+
148+
/**
149+
* Determine if the given infos correspond to the same env.
150+
*
151+
* @param environment1 - one of the two envs to compare
152+
* @param environment2 - one of the two envs to compare
153+
*/
154+
export function areSameEnvironment(
155+
environment1: PythonEnvInfo | string,
156+
environment2: PythonEnvInfo | string,
157+
): boolean {
158+
let path1: string;
159+
let path2: string;
160+
if (typeof environment1 === 'string') {
161+
path1 = environment1;
162+
} else {
163+
path1 = environment1.executable.filename;
164+
}
165+
if (typeof environment2 === 'string') {
166+
path2 = environment2;
167+
} else {
168+
path2 = environment2.executable.filename;
169+
}
170+
if (arePathsSame(path1, path2)) {
171+
return true;
172+
}
173+
return false;
174+
}

src/client/pythonEnvironments/base/locator.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export type PythonLocatorQuery = BasicPythonLocatorQuery & {
9292
searchLocations?: Uri[];
9393
};
9494

95-
type QueryForEvent<E> = E extends PythonEnvsChangedEvent ? PythonLocatorQuery : BasicPythonLocatorQuery;
95+
export type QueryForEvent<E> = E extends PythonEnvsChangedEvent ? PythonLocatorQuery : BasicPythonLocatorQuery;
9696

9797
/**
9898
* A single Python environment locator.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { cloneDeep, isEqual } from 'lodash';
5+
import { Event, EventEmitter } from 'vscode';
6+
import { traceVerbose } from '../../common/logger';
7+
import { createDeferred } from '../../common/utils/async';
8+
import { areSameEnvironment, PythonEnvInfo, PythonEnvKind } from '../base/info';
9+
import {
10+
ILocator, IPythonEnvsIterator, PythonEnvUpdatedEvent, QueryForEvent,
11+
} from '../base/locator';
12+
import { PythonEnvsChangedEvent } from '../base/watcher';
13+
14+
/**
15+
* Combines duplicate environments received from the incoming locator into one and passes on unique environments
16+
*/
17+
export class PythonEnvsReducer implements ILocator {
18+
public get onChanged(): Event<PythonEnvsChangedEvent> {
19+
return this.parentLocator.onChanged;
20+
}
21+
22+
constructor(private readonly parentLocator: ILocator) {}
23+
24+
public async resolveEnv(env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
25+
let environment: PythonEnvInfo | undefined;
26+
const waitForUpdatesDeferred = createDeferred<void>();
27+
const iterator = this.iterEnvs();
28+
iterator.onUpdated!((event) => {
29+
if (event === null) {
30+
waitForUpdatesDeferred.resolve();
31+
} else if (environment && areSameEnvironment(environment, event.new)) {
32+
environment = event.new;
33+
}
34+
});
35+
let result = await iterator.next();
36+
while (!result.done) {
37+
if (areSameEnvironment(result.value, env)) {
38+
environment = result.value;
39+
}
40+
// eslint-disable-next-line no-await-in-loop
41+
result = await iterator.next();
42+
}
43+
if (!environment) {
44+
return undefined;
45+
}
46+
await waitForUpdatesDeferred.promise;
47+
return this.parentLocator.resolveEnv(environment);
48+
}
49+
50+
public iterEnvs(query?: QueryForEvent<PythonEnvsChangedEvent>): IPythonEnvsIterator {
51+
const didUpdate = new EventEmitter<PythonEnvUpdatedEvent | null>();
52+
const incomingIterator = this.parentLocator.iterEnvs(query);
53+
const iterator: IPythonEnvsIterator = iterEnvsIterator(incomingIterator, didUpdate);
54+
iterator.onUpdated = didUpdate.event;
55+
return iterator;
56+
}
57+
}
58+
59+
async function* iterEnvsIterator(
60+
iterator: IPythonEnvsIterator,
61+
didUpdate: EventEmitter<PythonEnvUpdatedEvent | null>,
62+
): AsyncIterator<PythonEnvInfo, void> {
63+
const state = {
64+
done: false,
65+
pending: 0,
66+
};
67+
const seen: PythonEnvInfo[] = [];
68+
69+
if (iterator.onUpdated !== undefined) {
70+
iterator.onUpdated((event) => {
71+
if (event === null) {
72+
state.done = true;
73+
checkIfFinishedAndNotify(state, didUpdate);
74+
} else {
75+
const oldIndex = seen.findIndex((s) => areSameEnvironment(s, event.old));
76+
if (oldIndex !== -1) {
77+
state.pending += 1;
78+
resolveDifferencesInBackground(oldIndex, event.new, state, didUpdate, seen).ignoreErrors();
79+
} else {
80+
// This implies a problem in a downstream locator
81+
traceVerbose(`Expected already iterated env, got ${event.old}`);
82+
}
83+
}
84+
});
85+
}
86+
87+
let result = await iterator.next();
88+
while (!result.done) {
89+
const currEnv = result.value;
90+
const oldIndex = seen.findIndex((s) => areSameEnvironment(s, currEnv));
91+
if (oldIndex !== -1) {
92+
state.pending += 1;
93+
resolveDifferencesInBackground(oldIndex, currEnv, state, didUpdate, seen).ignoreErrors();
94+
} else {
95+
// We haven't yielded a matching env so yield this one as-is.
96+
yield currEnv;
97+
seen.push(currEnv);
98+
}
99+
// eslint-disable-next-line no-await-in-loop
100+
result = await iterator.next();
101+
}
102+
if (iterator.onUpdated === undefined) {
103+
state.done = true;
104+
checkIfFinishedAndNotify(state, didUpdate);
105+
}
106+
}
107+
108+
async function resolveDifferencesInBackground(
109+
oldIndex: number,
110+
newEnv: PythonEnvInfo,
111+
state: { done: boolean; pending: number },
112+
didUpdate: EventEmitter<PythonEnvUpdatedEvent | null>,
113+
seen: PythonEnvInfo[],
114+
) {
115+
const oldEnv = seen[oldIndex];
116+
const merged = mergeEnvironments(oldEnv, newEnv);
117+
if (!isEqual(oldEnv, merged)) {
118+
didUpdate.fire({ old: oldEnv, new: merged });
119+
seen[oldIndex] = merged;
120+
}
121+
state.pending -= 1;
122+
checkIfFinishedAndNotify(state, didUpdate);
123+
}
124+
125+
/**
126+
* When all info from incoming iterator has been received and all background calls finishes, notify that we're done
127+
* @param state Carries the current state of progress
128+
* @param didUpdate Used to notify when finished
129+
*/
130+
function checkIfFinishedAndNotify(
131+
state: { done: boolean; pending: number },
132+
didUpdate: EventEmitter<PythonEnvUpdatedEvent | null>,
133+
) {
134+
if (state.done && state.pending === 0) {
135+
didUpdate.fire(null);
136+
didUpdate.dispose();
137+
}
138+
}
139+
140+
export function mergeEnvironments(environment: PythonEnvInfo, other: PythonEnvInfo): PythonEnvInfo {
141+
const result = cloneDeep(environment);
142+
// Preserve type information.
143+
// Possible we identified environment as unknown, but a later provider has identified env type.
144+
if (environment.kind === PythonEnvKind.Unknown && other.kind && other.kind !== PythonEnvKind.Unknown) {
145+
result.kind = other.kind;
146+
}
147+
const props: (keyof PythonEnvInfo)[] = [
148+
'version',
149+
'kind',
150+
'executable',
151+
'name',
152+
'arch',
153+
'distro',
154+
'defaultDisplayName',
155+
'searchLocation',
156+
];
157+
props.forEach((prop) => {
158+
if (!result[prop] && other[prop]) {
159+
// tslint:disable: no-any
160+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
161+
(result as any)[prop] = other[prop];
162+
}
163+
});
164+
return result;
165+
}

src/test/pythonEnvironments/base/common.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
import { createDeferred, flattenIterator, iterable, mapToIterator } from '../../../client/common/utils/async';
4+
import { Event } from 'vscode';
5+
import {
6+
createDeferred, flattenIterator, iterable, mapToIterator,
7+
} from '../../../client/common/utils/async';
58
import { Architecture } from '../../../client/common/utils/platform';
69
import {
710
PythonEnvInfo,
811
PythonEnvKind,
912
} from '../../../client/pythonEnvironments/base/info';
1013
import { parseVersion } from '../../../client/pythonEnvironments/base/info/pythonVersion';
11-
import { IPythonEnvsIterator, Locator, PythonLocatorQuery } from '../../../client/pythonEnvironments/base/locator';
14+
import {
15+
IPythonEnvsIterator, Locator, PythonEnvUpdatedEvent, PythonLocatorQuery,
16+
} from '../../../client/pythonEnvironments/base/locator';
1217
import { PythonEnvsChangedEvent } from '../../../client/pythonEnvironments/base/watcher';
1318

1419
export function createEnv(
@@ -66,6 +71,7 @@ export class SimpleLocator extends Locator {
6671
resolve?: null | ((env: PythonEnvInfo) => Promise<PythonEnvInfo | undefined>);
6772
before?: Promise<void>;
6873
after?: Promise<void>;
74+
onUpdated?: Event<PythonEnvUpdatedEvent | null>;
6975
beforeEach?(e: PythonEnvInfo): Promise<void>;
7076
afterEach?(e: PythonEnvInfo): Promise<void>;
7177
onQuery?(query: PythonLocatorQuery | undefined, envs: PythonEnvInfo[]): Promise<PythonEnvInfo[]>;
@@ -83,7 +89,7 @@ export class SimpleLocator extends Locator {
8389
const deferred = this.deferred;
8490
const callbacks = this.callbacks;
8591
let envs = this.envs;
86-
async function* iterator() {
92+
const iterator: IPythonEnvsIterator = async function*() {
8793
if (callbacks?.onQuery !== undefined) {
8894
envs = await callbacks.onQuery(query, envs);
8995
}
@@ -114,8 +120,9 @@ export class SimpleLocator extends Locator {
114120
await callbacks.after;
115121
}
116122
deferred.resolve();
117-
}
118-
return iterator();
123+
}();
124+
iterator.onUpdated = this.callbacks?.onUpdated;
125+
return iterator;
119126
}
120127
public async resolveEnv(env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
121128
const envInfo: PythonEnvInfo = typeof env === 'string' ? createEnv('', '', undefined, env) : env;

0 commit comments

Comments
 (0)