Skip to content

Commit 81b6d8f

Browse files
kimadelineKartik Raj
and
Kartik Raj
authored
Environment info cache class (#14065)
* Add persistent storage external deps * PythonEnvInfoCache class + tests * Instantiate & initialize cache class in createAPI * Add extra test for flush() and initialize() * Env cache fixes: storage key + find() result check * Update src/client/pythonEnvironments/common/externalDependencies.ts Co-authored-by: Kartik Raj <[email protected]> * Use areSameEnvironment in getEnv * Don't ping persistent storage for every initialize * No need to export CompleteEnvInfoFunction * PythonEnvInfoCache doc comment * Rename createGlobalPersistentStoreStub to get... * Preemptively drop id key (#14051) * Return deep copies * IPersistentStore wrapper around IPersistentState * Use correct areSameEnvironment + fix stub * Remove obsolete comment * getEnv -> filterEnvs * Remove stubbing of areSameEnvironment * Update areSameEnv * Move IPersistentStateFactory registration to registerForIOC * Revert "Move IPersistentStateFactory registration to registerForIOC" This reverts commit edc6ce5. * Don't instantiate nor initialize cache for now Co-authored-by: Kartik Raj <[email protected]>
1 parent 6332b81 commit 81b6d8f

File tree

4 files changed

+296
-1
lines changed

4 files changed

+296
-1
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { cloneDeep } from 'lodash';
5+
import { getGlobalPersistentStore, IPersistentStore } from '../common/externalDependencies';
6+
import { PythonEnvInfo } from './info';
7+
import { areSameEnv } from './info/env';
8+
9+
/**
10+
* Represents the environment info cache to be used by the cache locator.
11+
*/
12+
export interface IEnvsCache {
13+
/**
14+
* Initialization logic to be done outside of the constructor, for example reading from persistent storage.
15+
*/
16+
initialize(): void;
17+
18+
/**
19+
* Return all environment info currently in memory for this session.
20+
*
21+
* @return An array of cached environment info, or `undefined` if there are none.
22+
*/
23+
getAllEnvs(): PythonEnvInfo[] | undefined;
24+
25+
/**
26+
* Replace all environment info currently in memory for this session.
27+
*
28+
* @param envs The array of environment info to store in the in-memory cache.
29+
*/
30+
setAllEnvs(envs: PythonEnvInfo[]): void;
31+
32+
/**
33+
* If the cache has been initialized, return environmnent info objects that match a query object.
34+
* If none of the environments in the cache match the query data, return an empty array.
35+
* If the in-memory cache has not been initialized prior to calling `filterEnvs`, return `undefined`.
36+
*
37+
* @param env The environment info data that will be used to look for
38+
* environment info objects in the cache, or a unique environment key.
39+
* If passing an environment info object, it may contain incomplete environment info.
40+
* @return The environment info objects matching the `env` param,
41+
* or `undefined` if the in-memory cache is not initialized.
42+
*/
43+
filterEnvs(env: PythonEnvInfo | string): PythonEnvInfo[] | undefined;
44+
45+
/**
46+
* Writes the content of the in-memory cache to persistent storage.
47+
*/
48+
flush(): Promise<void>;
49+
}
50+
51+
type CompleteEnvInfoFunction = (envInfo: PythonEnvInfo) => boolean;
52+
53+
/**
54+
* Environment info cache using persistent storage to save and retrieve pre-cached env info.
55+
*/
56+
export class PythonEnvInfoCache implements IEnvsCache {
57+
private initialized = false;
58+
59+
private envsList: PythonEnvInfo[] | undefined;
60+
61+
private persistentStorage: IPersistentStore<PythonEnvInfo[]> | undefined;
62+
63+
constructor(private readonly isComplete: CompleteEnvInfoFunction) {}
64+
65+
public initialize(): void {
66+
if (this.initialized) {
67+
return;
68+
}
69+
70+
this.initialized = true;
71+
this.persistentStorage = getGlobalPersistentStore<PythonEnvInfo[]>('PYTHON_ENV_INFO_CACHE');
72+
this.envsList = this.persistentStorage?.get();
73+
}
74+
75+
public getAllEnvs(): PythonEnvInfo[] | undefined {
76+
return cloneDeep(this.envsList);
77+
}
78+
79+
public setAllEnvs(envs: PythonEnvInfo[]): void {
80+
this.envsList = cloneDeep(envs);
81+
}
82+
83+
public filterEnvs(env: PythonEnvInfo | string): PythonEnvInfo[] | undefined {
84+
const result = this.envsList?.filter((info) => areSameEnv(info, env));
85+
86+
if (result) {
87+
return cloneDeep(result);
88+
}
89+
90+
return undefined;
91+
}
92+
93+
public async flush(): Promise<void> {
94+
const completeEnvs = this.envsList?.filter(this.isComplete);
95+
96+
if (completeEnvs?.length) {
97+
await this.persistentStorage?.set(completeEnvs);
98+
}
99+
}
100+
}

src/client/pythonEnvironments/common/externalDependencies.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import * as fsapi from 'fs-extra';
55
import * as path from 'path';
66
import { ExecutionResult, IProcessServiceFactory } from '../../common/process/types';
7+
import { IPersistentStateFactory } from '../../common/types';
78
import { getOSType, OSType } from '../../common/utils/platform';
89
import { IServiceContainer } from '../../ioc/types';
910

@@ -37,3 +38,22 @@ export function arePathsSame(path1: string, path2: string): boolean {
3738
}
3839
return path1 === path2;
3940
}
41+
42+
function getPersistentStateFactory(): IPersistentStateFactory {
43+
return internalServiceContainer.get<IPersistentStateFactory>(IPersistentStateFactory);
44+
}
45+
46+
export interface IPersistentStore<T> {
47+
get(): T | undefined;
48+
set(value: T): Promise<void>;
49+
}
50+
51+
export function getGlobalPersistentStore<T>(key: string): IPersistentStore<T> {
52+
const factory = getPersistentStateFactory();
53+
const state = factory.createGlobalPersistentState<T>(key, undefined);
54+
55+
return {
56+
get() { return state.value; },
57+
set(value: T) { return state.updateValue(value); },
58+
};
59+
}

src/client/pythonEnvironments/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function createAPI(): [PythonEnvironments, () => void] {
5555
() => {
5656
activateLocators();
5757
// Any other activation needed for the API will go here later.
58-
}
58+
},
5959
];
6060
}
6161

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as assert from 'assert';
5+
import * as sinon from 'sinon';
6+
import { PythonEnvInfoCache } from '../../../client/pythonEnvironments/base/envsCache';
7+
import { PythonEnvInfo, PythonEnvKind } from '../../../client/pythonEnvironments/base/info';
8+
import * as externalDependencies from '../../../client/pythonEnvironments/common/externalDependencies';
9+
10+
suite('Environment Info cache', () => {
11+
let getGlobalPersistentStoreStub: sinon.SinonStub;
12+
let updatedValues: PythonEnvInfo[] | undefined;
13+
14+
const allEnvsComplete = () => true;
15+
const envInfoArray = [
16+
{
17+
kind: PythonEnvKind.Conda, executable: { filename: 'my-conda-env' },
18+
},
19+
{
20+
kind: PythonEnvKind.Venv, executable: { filename: 'my-venv-env' },
21+
},
22+
{
23+
kind: PythonEnvKind.Pyenv, executable: { filename: 'my-pyenv-env' },
24+
},
25+
] as PythonEnvInfo[];
26+
27+
setup(() => {
28+
getGlobalPersistentStoreStub = sinon.stub(externalDependencies, 'getGlobalPersistentStore');
29+
getGlobalPersistentStoreStub.returns({
30+
get() { return envInfoArray; },
31+
set(envs: PythonEnvInfo[]) {
32+
updatedValues = envs;
33+
return Promise.resolve();
34+
},
35+
});
36+
});
37+
38+
teardown(() => {
39+
getGlobalPersistentStoreStub.restore();
40+
updatedValues = undefined;
41+
});
42+
43+
test('`initialize` reads from persistent storage', () => {
44+
const envsCache = new PythonEnvInfoCache(allEnvsComplete);
45+
46+
envsCache.initialize();
47+
48+
assert.ok(getGlobalPersistentStoreStub.calledOnce);
49+
});
50+
51+
test('The in-memory env info array is undefined if there is no value in persistent storage when initializing the cache', () => {
52+
const envsCache = new PythonEnvInfoCache(allEnvsComplete);
53+
54+
getGlobalPersistentStoreStub.returns({ get() { return undefined; } });
55+
envsCache.initialize();
56+
const result = envsCache.getAllEnvs();
57+
58+
assert.strictEqual(result, undefined);
59+
});
60+
61+
test('`getAllEnvs` should return a deep copy of the environments currently in memory', () => {
62+
const envsCache = new PythonEnvInfoCache(allEnvsComplete);
63+
64+
envsCache.initialize();
65+
const envs = envsCache.getAllEnvs()!;
66+
67+
envs[0].name = 'some-other-name';
68+
69+
assert.ok(envs[0] !== envInfoArray[0]);
70+
});
71+
72+
test('`getAllEnvs` should return undefined if nothing has been set', () => {
73+
const envsCache = new PythonEnvInfoCache(allEnvsComplete);
74+
75+
const envs = envsCache.getAllEnvs();
76+
77+
assert.deepStrictEqual(envs, undefined);
78+
});
79+
80+
test('`setAllEnvs` should clone the environment info array passed as a parameter', () => {
81+
const envsCache = new PythonEnvInfoCache(allEnvsComplete);
82+
83+
envsCache.setAllEnvs(envInfoArray);
84+
const envs = envsCache.getAllEnvs();
85+
86+
assert.deepStrictEqual(envs, envInfoArray);
87+
assert.strictEqual(envs === envInfoArray, false);
88+
});
89+
90+
test('`filterEnvs` should return environments that match its argument using areSameEnvironmnet', () => {
91+
const env:PythonEnvInfo = { executable: { filename: 'my-venv-env' } } as unknown as PythonEnvInfo;
92+
const envsCache = new PythonEnvInfoCache(allEnvsComplete);
93+
94+
envsCache.initialize();
95+
96+
const result = envsCache.filterEnvs(env);
97+
98+
assert.deepStrictEqual(result, [{
99+
kind: PythonEnvKind.Venv, executable: { filename: 'my-venv-env' },
100+
}]);
101+
});
102+
103+
test('`filterEnvs` should return a deep copy of the matched environments', () => {
104+
const envToFind = {
105+
kind: PythonEnvKind.System, executable: { filename: 'my-system-env' },
106+
} as unknown as PythonEnvInfo;
107+
const env:PythonEnvInfo = { executable: { filename: 'my-system-env' } } as unknown as PythonEnvInfo;
108+
const envsCache = new PythonEnvInfoCache(allEnvsComplete);
109+
110+
envsCache.setAllEnvs([...envInfoArray, envToFind]);
111+
112+
const result = envsCache.filterEnvs(env)!;
113+
result[0].name = 'some-other-name';
114+
115+
assert.notDeepStrictEqual(result[0], envToFind);
116+
});
117+
118+
test('`filterEnvs` should return an empty array if no environment matches the properties of its argument', () => {
119+
const env:PythonEnvInfo = { executable: { filename: 'my-nonexistent-env' } } as unknown as PythonEnvInfo;
120+
const envsCache = new PythonEnvInfoCache(allEnvsComplete);
121+
122+
envsCache.initialize();
123+
124+
const result = envsCache.filterEnvs(env);
125+
126+
assert.deepStrictEqual(result, []);
127+
});
128+
129+
test('`filterEnvs` should return undefined if the cache hasn\'t been initialized', () => {
130+
const env:PythonEnvInfo = { executable: { filename: 'my-nonexistent-env' } } as unknown as PythonEnvInfo;
131+
const envsCache = new PythonEnvInfoCache(allEnvsComplete);
132+
133+
const result = envsCache.filterEnvs(env);
134+
135+
assert.strictEqual(result, undefined);
136+
});
137+
138+
test('`flush` should write complete environment info objects to persistent storage', async () => {
139+
const otherEnv = {
140+
kind: PythonEnvKind.OtherGlobal,
141+
executable: { filename: 'my-other-env' },
142+
defaultDisplayName: 'other-env',
143+
};
144+
const updatedEnvInfoArray = [
145+
otherEnv, { kind: PythonEnvKind.System, executable: { filename: 'my-system-env' } },
146+
] as PythonEnvInfo[];
147+
const expected = [
148+
otherEnv,
149+
];
150+
const envsCache = new PythonEnvInfoCache((env) => env.defaultDisplayName !== undefined);
151+
152+
envsCache.initialize();
153+
envsCache.setAllEnvs(updatedEnvInfoArray);
154+
await envsCache.flush();
155+
156+
assert.deepStrictEqual(updatedValues, expected);
157+
});
158+
159+
test('`flush` should not write to persistent storage if there are no environment info objects in-memory', async () => {
160+
const envsCache = new PythonEnvInfoCache((env) => env.kind === PythonEnvKind.MacDefault);
161+
162+
await envsCache.flush();
163+
164+
assert.strictEqual(updatedValues, undefined);
165+
});
166+
167+
test('`flush` should not write to persistent storage if there are no complete environment info objects', async () => {
168+
const envsCache = new PythonEnvInfoCache((env) => env.kind === PythonEnvKind.MacDefault);
169+
170+
envsCache.initialize();
171+
await envsCache.flush();
172+
173+
assert.strictEqual(updatedValues, undefined);
174+
});
175+
});

0 commit comments

Comments
 (0)