Skip to content

Commit 569887d

Browse files
author
Kartik Raj
committed
Add environments reducer
1 parent bc9cd8b commit 569887d

File tree

4 files changed

+180
-1
lines changed

4 files changed

+180
-1
lines changed

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

Lines changed: 20 additions & 0 deletions
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,22 @@ 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,
156+
environment2: PythonEnvInfo,
157+
): boolean {
158+
if (!environment1 || !environment2) {
159+
return false;
160+
}
161+
if (arePathsSame(environment1.executable.filename, environment2.executable.filename)) {
162+
return true;
163+
}
164+
return false;
165+
}

src/client/pythonEnvironments/base/locator.ts

Lines changed: 1 addition & 1 deletion
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.
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { Event, EventEmitter } from 'vscode';
5+
import { areSameEnvironment, PythonEnvInfo, PythonEnvKind } from '../base/info';
6+
import {
7+
ILocator, IPythonEnvsIterator, PythonEnvUpdatedEvent, QueryForEvent,
8+
} from '../base/locator';
9+
import { PythonEnvsChangedEvent } from '../base/watcher';
10+
11+
type IteratorState = {
12+
/**
13+
* Set to true when all information from incoming iterator has been received
14+
*/
15+
done: boolean;
16+
/**
17+
* Carries the number of pending background calls ongoing at the moment
18+
*/
19+
pending: number;
20+
};
21+
22+
export class PythonEnvsReducer implements ILocator {
23+
public get onChanged(): Event<PythonEnvsChangedEvent> {
24+
return this.pythonEnvsManager.onChanged;
25+
}
26+
27+
constructor(private readonly pythonEnvsManager: ILocator) {}
28+
29+
public resolveEnv(env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
30+
return this.pythonEnvsManager.resolveEnv(env);
31+
}
32+
33+
public iterEnvs(query?: QueryForEvent<PythonEnvsChangedEvent>): IPythonEnvsIterator {
34+
const didUpdate = new EventEmitter<PythonEnvUpdatedEvent | null>();
35+
const iterator: IPythonEnvsIterator = this.iterEnvsIterator(didUpdate, query);
36+
iterator.onUpdated = didUpdate.event;
37+
return iterator;
38+
}
39+
40+
private async* iterEnvsIterator(
41+
didUpdate: EventEmitter<PythonEnvUpdatedEvent | null>,
42+
query?: QueryForEvent<PythonEnvsChangedEvent>,
43+
): AsyncIterator<PythonEnvInfo, void> {
44+
const state = {
45+
done: false,
46+
pending: 0,
47+
};
48+
const seen: PythonEnvInfo[] = [];
49+
const iterator = this.pythonEnvsManager.iterEnvs(query);
50+
51+
if (iterator.onUpdated !== undefined) {
52+
iterator.onUpdated((event) => {
53+
if (event === null) {
54+
state.done = true;
55+
checkIfFinishedAndNotify(state, didUpdate);
56+
} else {
57+
const old = seen.find((s) => areSameEnvironment(s, event.old));
58+
if (old !== undefined) {
59+
state.pending += 1;
60+
resolveDifferencesInBackground(old, event.new, state, didUpdate).ignoreErrors();
61+
}
62+
}
63+
});
64+
}
65+
66+
let result = await iterator.next();
67+
while (!result.done) {
68+
const currEnv = result.value;
69+
const old = seen.find((s) => areSameEnvironment(s, currEnv));
70+
if (old !== undefined) {
71+
state.pending += 1;
72+
resolveDifferencesInBackground(old, currEnv, state, didUpdate).ignoreErrors();
73+
} else {
74+
yield currEnv;
75+
seen.push(currEnv);
76+
}
77+
// eslint-disable-next-line no-await-in-loop
78+
result = await iterator.next();
79+
}
80+
if (iterator.onUpdated === undefined) {
81+
state.done = true;
82+
}
83+
}
84+
}
85+
86+
async function resolveDifferencesInBackground(
87+
oldEnv: PythonEnvInfo,
88+
newEnv: PythonEnvInfo,
89+
state: IteratorState,
90+
didUpdate: EventEmitter<PythonEnvUpdatedEvent | null>,
91+
) {
92+
const merged = mergeEnvironments(oldEnv, newEnv);
93+
didUpdate.fire({ old: oldEnv, new: merged });
94+
state.pending -= 1;
95+
checkIfFinishedAndNotify(state, didUpdate);
96+
}
97+
98+
/**
99+
* When all info from incoming iterator has been received and all background calls finishes, notify that we're done
100+
* @param state Carries the current state of progress
101+
* @param didUpdate Used to notify when finished
102+
*/
103+
function checkIfFinishedAndNotify(
104+
state: IteratorState,
105+
didUpdate: EventEmitter<PythonEnvUpdatedEvent | null>,
106+
) {
107+
if (state.done && state.pending === 0) {
108+
didUpdate.fire(null);
109+
didUpdate.dispose();
110+
}
111+
}
112+
113+
export function mergeEnvironments(environment: PythonEnvInfo, other: PythonEnvInfo): PythonEnvInfo {
114+
// Preserve type information.
115+
// Possible we identified environment as unknown, but a later provider has identified env type.
116+
if (environment.kind === PythonEnvKind.Unknown && other.kind && other.kind !== PythonEnvKind.Unknown) {
117+
environment.kind = other.kind;
118+
}
119+
const props: (keyof PythonEnvInfo)[] = [
120+
'version', 'kind', 'executable', 'name', 'arch', 'distro', 'defaultDisplayName', 'searchLocation',
121+
];
122+
props.forEach((prop) => {
123+
if (!environment[prop] && other[prop]) {
124+
// tslint:disable: no-any
125+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
126+
(environment as any)[prop] = other[prop];
127+
}
128+
});
129+
return environment;
130+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { assert } from 'chai';
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
// Licensed under the MIT License.
4+
5+
import { PythonEnvKind } from '../../../client/pythonEnvironments/base/info';
6+
import { PythonEnvsReducer } from '../../../client/pythonEnvironments/collection/environmentsReducer';
7+
import {
8+
createLocatedEnv, getEnvs, SimpleLocator,
9+
} from '../base/common';
10+
11+
suite('Environments Reducer', () => {
12+
test('Duplicated incoming environments from locator manager are removed', async () => {
13+
const env1 = createLocatedEnv('path/to/env1', '3.5.12b1', PythonEnvKind.Venv);
14+
const env2 = createLocatedEnv('path/to/env2', '3.8.1', PythonEnvKind.Conda);
15+
const env3 = createLocatedEnv('path/to/env3', '2.7', PythonEnvKind.System);
16+
const env4 = createLocatedEnv('path/to/env2', '3.9.0rc2', PythonEnvKind.Pyenv);
17+
const env5 = createLocatedEnv('path/to/env1', '3.8', PythonEnvKind.System);
18+
const environments = [env1, env2, env3, env4, env5];
19+
const pythonEnvManager = new SimpleLocator(environments);
20+
21+
const reducer = new PythonEnvsReducer(pythonEnvManager);
22+
23+
const iterator = reducer.iterEnvs();
24+
const envs = await getEnvs(iterator);
25+
26+
const expected = [env1, env2, env3];
27+
assert.deepEqual(envs.sort(), expected.sort());
28+
});
29+
});

0 commit comments

Comments
 (0)