Skip to content

Commit 42e2474

Browse files
author
Kartik Raj
committed
Validate selected interpreter
1 parent bb9d4dd commit 42e2474

File tree

5 files changed

+68
-32
lines changed

5 files changed

+68
-32
lines changed

src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { Event } from 'vscode';
55
import { traceInfo } from '../../../../logging';
66
import { reportInterpretersChanged } from '../../../../proposedApi';
7-
import { arePathsSame, pathExists } from '../../../common/externalDependencies';
7+
import { arePathsSame, getFileInfo, pathExists } from '../../../common/externalDependencies';
88
import { PythonEnvInfo } from '../../info';
99
import { areSameEnv, getEnvPath } from '../../info/env';
1010
import {
@@ -33,18 +33,19 @@ export interface IEnvsCollectionCache {
3333
/**
3434
* Adds environment to cache.
3535
*/
36-
addEnv(env: PythonEnvInfo, hasCompleteInfo?: boolean): void;
36+
addEnv(env: PythonEnvInfo, hasLatestInfo?: boolean): void;
3737

3838
/**
3939
* Return cached environment information for a given path if it exists and
40-
* has complete info, otherwise return `undefined`.
40+
* is up to date, otherwise return `undefined`.
4141
*
4242
* @param path - Python executable path or path to environment
4343
*/
44-
getCompleteInfo(path: string): PythonEnvInfo | undefined;
44+
getLatestInfo(path: string): Promise<PythonEnvInfo | undefined>;
4545

4646
/**
47-
* Writes the content of the in-memory cache to persistent storage.
47+
* Writes the content of the in-memory cache to persistent storage. It is assumed
48+
* all envs have upto date info when this is called.
4849
*/
4950
flush(): Promise<void>;
5051

@@ -60,7 +61,7 @@ export interface IEnvsCollectionCache {
6061
clearCache(): Promise<void>;
6162
}
6263

63-
export type PythonEnvCompleteInfo = { hasCompleteInfo?: boolean } & PythonEnvInfo;
64+
export type PythonEnvLatestInfo = { hasLatestInfo?: boolean } & PythonEnvInfo;
6465

6566
interface IPersistentStorage {
6667
load(): Promise<PythonEnvInfo[]>;
@@ -72,7 +73,7 @@ interface IPersistentStorage {
7273
*/
7374
export class PythonEnvInfoCache extends PythonEnvsWatcher<PythonEnvCollectionChangedEvent>
7475
implements IEnvsCollectionCache {
75-
private envs: PythonEnvCompleteInfo[] = [];
76+
private envs: PythonEnvLatestInfo[] = [];
7677

7778
constructor(private readonly persistentStorage: IPersistentStorage) {
7879
super();
@@ -103,10 +104,11 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher<PythonEnvCollectionCha
103104
return this.envs;
104105
}
105106

106-
public addEnv(env: PythonEnvCompleteInfo, hasCompleteInfo?: boolean): void {
107+
public addEnv(env: PythonEnvLatestInfo, hasLatestInfo?: boolean): void {
107108
const found = this.envs.find((e) => areSameEnv(e, env));
108-
if (hasCompleteInfo) {
109-
env.hasCompleteInfo = true;
109+
if (hasLatestInfo) {
110+
env.hasLatestInfo = true;
111+
this.flush(false).ignoreErrors();
110112
}
111113
if (!found) {
112114
this.envs.push(env);
@@ -133,26 +135,33 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher<PythonEnvCollectionCha
133135
}
134136
}
135137

136-
public getCompleteInfo(path: string): PythonEnvInfo | undefined {
138+
public async getLatestInfo(path: string): Promise<PythonEnvInfo | undefined> {
137139
// `path` can either be path to environment or executable path
138-
let env = this.envs.find((e) => arePathsSame(e.location, path));
139-
if (env?.hasCompleteInfo) {
140+
const env = this.envs.find((e) => arePathsSame(e.location, path)) ?? this.envs.find((e) => areSameEnv(e, path));
141+
if (env?.hasLatestInfo) {
140142
return env;
141143
}
142-
env = this.envs.find((e) => areSameEnv(e, path));
143-
return env?.hasCompleteInfo ? env : undefined;
144+
if (env && (env?.hasLatestInfo || (await validateInfo(env)))) {
145+
return env;
146+
}
147+
return undefined;
144148
}
145149

146150
public async clearAndReloadFromStorage(): Promise<void> {
147151
this.envs = await this.persistentStorage.load();
152+
this.envs.forEach((e) => {
153+
e.hasLatestInfo = false;
154+
});
148155
}
149156

150-
public async flush(): Promise<void> {
157+
public async flush(allEnvsHaveLatestInfo = true): Promise<void> {
151158
if (this.envs.length) {
152159
traceInfo('Environments added to cache', JSON.stringify(this.envs));
153-
this.envs.forEach((e) => {
154-
e.hasCompleteInfo = true;
155-
});
160+
if (allEnvsHaveLatestInfo) {
161+
this.envs.forEach((e) => {
162+
e.hasLatestInfo = true;
163+
});
164+
}
156165
await this.persistentStorage.store(this.envs);
157166
}
158167
}
@@ -167,6 +176,16 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher<PythonEnvCollectionCha
167176
}
168177
}
169178

179+
async function validateInfo(env: PythonEnvInfo) {
180+
const { ctime, mtime } = await getFileInfo(env.executable.filename);
181+
if (ctime === env.executable.ctime && mtime === env.executable.mtime) {
182+
return true;
183+
}
184+
env.executable.ctime = ctime;
185+
env.executable.mtime = mtime;
186+
return false;
187+
}
188+
170189
/**
171190
* Build a cache of PythonEnvInfo that is ready to use.
172191
*/

src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export class EnvsCollectionService extends PythonEnvsWatcher<PythonEnvCollection
8181
// Note cache may have incomplete info when a refresh is happening.
8282
// This API is supposed to return complete info by definition, so
8383
// only use cache if it has complete info on an environment.
84-
const cachedEnv = this.cache.getCompleteInfo(path);
84+
const cachedEnv = await this.cache.getLatestInfo(path);
8585
if (cachedEnv) {
8686
return cachedEnv;
8787
}

src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
getInterpreterPathFromDir,
1818
getPythonVersionFromPath,
1919
} from '../../../common/commonUtils';
20-
import { arePathsSame, getWorkspaceFolders, isParentPath } from '../../../common/externalDependencies';
20+
import { arePathsSame, getFileInfo, getWorkspaceFolders, isParentPath } from '../../../common/externalDependencies';
2121
import { AnacondaCompanyName, Conda, isCondaEnvironment } from '../../../common/environmentManagers/conda';
2222
import { getPyenvVersionsDir, parsePyenvVersion } from '../../../common/environmentManagers/pyenv';
2323
import { Architecture, getOSType, OSType } from '../../../../common/utils/platform';
@@ -59,6 +59,9 @@ export async function resolveBasicEnv(env: BasicEnvInfo, useCache = false): Prom
5959
}
6060
setEnvDisplayString(resolvedEnv);
6161
resolvedEnv.id = getEnvID(resolvedEnv.executable.filename, resolvedEnv.location);
62+
const { ctime, mtime } = await getFileInfo(resolvedEnv.executable.filename);
63+
resolvedEnv.executable.ctime = ctime;
64+
resolvedEnv.executable.mtime = mtime;
6265
return resolvedEnv;
6366
}
6467

src/client/pythonEnvironments/common/externalDependencies.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,20 @@ export async function resolveSymbolicLink(absPath: string, stats?: fsapi.Stats):
102102
return absPath;
103103
}
104104

105+
export async function getFileInfo(filePath: string): Promise<{ ctime: number; mtime: number }> {
106+
try {
107+
const data = await fsapi.lstat(filePath);
108+
return {
109+
ctime: data.ctime.valueOf(),
110+
mtime: data.mtime.valueOf(),
111+
};
112+
} catch (ex) {
113+
// This can fail on some cases, such as, `reparse points` on windows. So, return the
114+
// time as -1. Which we treat as not set in the extension.
115+
return { ctime: -1, mtime: -1 };
116+
}
117+
}
118+
105119
export async function isFile(filePath: string): Promise<boolean> {
106120
const stats = await fsapi.lstat(filePath);
107121
if (stats.isSymbolicLink()) {

src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
} from '../../../../../client/pythonEnvironments/base/locator';
1919
import {
2020
createCollectionCache,
21-
PythonEnvCompleteInfo,
21+
PythonEnvLatestInfo,
2222
} from '../../../../../client/pythonEnvironments/base/locators/composite/envsCollectionCache';
2323
import { EnvsCollectionService } from '../../../../../client/pythonEnvironments/base/locators/composite/envsCollectionService';
2424
import { PythonEnvCollectionChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher';
@@ -100,13 +100,13 @@ suite('Python envs locator - Environments Collection', async () => {
100100
updatedName,
101101
);
102102
if (doNotIncludeCached) {
103-
return [env1, env2, env3].map((e: PythonEnvCompleteInfo) => {
104-
e.hasCompleteInfo = true;
103+
return [env1, env2, env3].map((e: PythonEnvLatestInfo) => {
104+
e.hasLatestInfo = true;
105105
return e;
106106
});
107107
}
108-
return [envCached1, env1, env2, env3].map((e: PythonEnvCompleteInfo) => {
109-
e.hasCompleteInfo = true;
108+
return [envCached1, env1, env2, env3].map((e: PythonEnvLatestInfo) => {
109+
e.hasLatestInfo = true;
110110
return e;
111111
});
112112
}
@@ -545,8 +545,8 @@ suite('Python envs locator - Environments Collection', async () => {
545545
test('resolveEnv() uses cache if complete info is available', async () => {
546546
const resolvedViaLocator = buildEnvInfo({ executable: 'Resolved via locator' });
547547
const cachedEnvs = getCachedEnvs();
548-
const env: PythonEnvCompleteInfo = cachedEnvs[0];
549-
env.hasCompleteInfo = true; // Has complete info
548+
const env: PythonEnvLatestInfo = cachedEnvs[0];
549+
env.hasLatestInfo = true; // Has complete info
550550
const parentLocator = new SimpleLocator([], {
551551
resolve: async (e: PythonEnvInfo) => {
552552
if (env.executable.filename === e.executable.filename) {
@@ -568,8 +568,8 @@ suite('Python envs locator - Environments Collection', async () => {
568568
test('resolveEnv() uses underlying locator if cache does not have complete info for env', async () => {
569569
const resolvedViaLocator = buildEnvInfo({ executable: 'Resolved via locator' });
570570
const cachedEnvs = getCachedEnvs();
571-
const env: PythonEnvCompleteInfo = cachedEnvs[0];
572-
env.hasCompleteInfo = false; // Does not have complete info
571+
const env: PythonEnvLatestInfo = cachedEnvs[0];
572+
env.hasLatestInfo = false; // Does not have complete info
573573
const parentLocator = new SimpleLocator([], {
574574
resolve: async (e: PythonEnvInfo) => {
575575
if (env.executable.filename === e.executable.filename) {
@@ -618,11 +618,11 @@ suite('Python envs locator - Environments Collection', async () => {
618618
store: async () => noop(),
619619
});
620620
collectionService = new EnvsCollectionService(cache, parentLocator);
621-
const resolved: PythonEnvCompleteInfo | undefined = await collectionService.resolveEnv(
621+
const resolved: PythonEnvLatestInfo | undefined = await collectionService.resolveEnv(
622622
resolvedViaLocator.executable.filename,
623623
);
624624
const envs = collectionService.getEnvs();
625-
expect(resolved?.hasCompleteInfo).to.equal(true);
625+
expect(resolved?.hasLatestInfo).to.equal(true);
626626
assertEnvsEqual(envs, [resolved]);
627627
sinon.assert.calledOnceWithExactly(reportInterpretersChangedStub, [
628628
{ path: resolved?.executable.filename, type: 'add' },

0 commit comments

Comments
 (0)