Skip to content

Commit 31d2568

Browse files
author
Kartik Raj
committed
Add environments reducer
1 parent bc9cd8b commit 31d2568

File tree

4 files changed

+177
-1
lines changed

4 files changed

+177
-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: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
export class PythonEnvsReducer implements ILocator {
12+
public get onChanged(): Event<PythonEnvsChangedEvent> {
13+
return this.pythonEnvsManager.onChanged;
14+
}
15+
16+
constructor(private readonly pythonEnvsManager: ILocator) {}
17+
18+
public resolveEnv(env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
19+
return this.pythonEnvsManager.resolveEnv(env);
20+
}
21+
22+
public iterEnvs(query?: QueryForEvent<PythonEnvsChangedEvent>): IPythonEnvsIterator {
23+
const didUpdate = new EventEmitter<PythonEnvUpdatedEvent | null>();
24+
const iterator: IPythonEnvsIterator = this.iterEnvsIterator(didUpdate, query);
25+
iterator.onUpdated = didUpdate.event;
26+
return iterator;
27+
}
28+
29+
private async* iterEnvsIterator(
30+
didUpdate: EventEmitter<PythonEnvUpdatedEvent | null>,
31+
query?: QueryForEvent<PythonEnvsChangedEvent>,
32+
): AsyncIterator<PythonEnvInfo, void> {
33+
const state = {
34+
done: false,
35+
pending: 0,
36+
};
37+
const seen: PythonEnvInfo[] = [];
38+
const iterator = this.pythonEnvsManager.iterEnvs(query);
39+
40+
if (iterator.onUpdated !== undefined) {
41+
iterator.onUpdated((event) => {
42+
if (event === null) {
43+
state.done = true;
44+
checkIfFinishedAndNotify(state, didUpdate);
45+
} else {
46+
const old = seen.find((s) => areSameEnvironment(s, event.old));
47+
if (old !== undefined) {
48+
state.pending += 1;
49+
resolveDifferencesInBackground(old, event.new, { seen, ...state }, didUpdate).ignoreErrors();
50+
}
51+
}
52+
});
53+
}
54+
55+
let result = await iterator.next();
56+
while (!result.done) {
57+
const currEnv = result.value;
58+
const old = seen.find((s) => areSameEnvironment(s, currEnv));
59+
if (old !== undefined) {
60+
state.pending += 1;
61+
resolveDifferencesInBackground(old, currEnv, { seen, ...state }, didUpdate).ignoreErrors();
62+
} else {
63+
yield currEnv;
64+
seen.push(currEnv);
65+
}
66+
// eslint-disable-next-line no-await-in-loop
67+
result = await iterator.next();
68+
}
69+
if (iterator.onUpdated === undefined) {
70+
state.done = true;
71+
}
72+
}
73+
}
74+
75+
async function resolveDifferencesInBackground(
76+
oldEnv: PythonEnvInfo,
77+
newEnv: PythonEnvInfo,
78+
state: { seen: PythonEnvInfo[]; done: boolean; pending: number },
79+
didUpdate: EventEmitter<PythonEnvUpdatedEvent | null>,
80+
) {
81+
const merged = mergeEnvironments(oldEnv, newEnv);
82+
didUpdate.fire({ old: oldEnv, new: merged });
83+
state.pending -= 1;
84+
state.seen[state.seen.indexOf(oldEnv)] = merged;
85+
checkIfFinishedAndNotify(state, didUpdate);
86+
}
87+
88+
/**
89+
* When all info from incoming iterator has been received and all background calls finishes, notify that we're done
90+
* @param state Carries the current state of progress
91+
* @param didUpdate Used to notify when finished
92+
*/
93+
function checkIfFinishedAndNotify(
94+
state: { done: boolean; pending: number },
95+
didUpdate: EventEmitter<PythonEnvUpdatedEvent | null>,
96+
) {
97+
if (state.done && state.pending === 0) {
98+
didUpdate.fire(null);
99+
didUpdate.dispose();
100+
}
101+
}
102+
103+
export function mergeEnvironments(environment: PythonEnvInfo, other: PythonEnvInfo): PythonEnvInfo {
104+
// Preserve type information.
105+
// Possible we identified environment as unknown, but a later provider has identified env type.
106+
if (environment.kind === PythonEnvKind.Unknown && other.kind && other.kind !== PythonEnvKind.Unknown) {
107+
environment.kind = other.kind;
108+
}
109+
const props: (keyof PythonEnvInfo)[] = [
110+
'version',
111+
'kind',
112+
'executable',
113+
'name',
114+
'arch',
115+
'distro',
116+
'defaultDisplayName',
117+
'searchLocation',
118+
];
119+
props.forEach((prop) => {
120+
if (!environment[prop] && other[prop]) {
121+
// tslint:disable: no-any
122+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
123+
(environment as any)[prop] = other[prop];
124+
}
125+
});
126+
return environment;
127+
}
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)