Skip to content

Commit 43c059e

Browse files
author
Kartik Raj
authored
Resolve conda environments without relying on the updated list of conda envs (#20112)
Closes #20069 Closes #20110 Closes #20070
1 parent 1db65b8 commit 43c059e

File tree

7 files changed

+128
-140
lines changed

7 files changed

+128
-140
lines changed

src/client/proposedApi.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -271,19 +271,19 @@ export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment
271271
path,
272272
id: getEnvID(path),
273273
executable: {
274-
uri: Uri.file(env.executable.filename),
274+
uri: env.executable.filename === 'python' ? undefined : Uri.file(env.executable.filename),
275275
bitness: convertBitness(env.arch),
276276
sysPrefix: env.executable.sysPrefix,
277277
},
278278
environment: env.type
279279
? {
280280
type: convertEnvType(env.type),
281-
name: env.name,
281+
name: env.name === '' ? undefined : env.name,
282282
folderUri: Uri.file(env.location),
283283
workspaceFolder: env.searchLocation,
284284
}
285285
: undefined,
286-
version: version as ResolvedEnvironment['version'],
286+
version: env.executable.filename === 'python' ? undefined : (version as ResolvedEnvironment['version']),
287287
tools: tool ? [tool] : [],
288288
};
289289
return resolvedEnv;
@@ -325,19 +325,16 @@ export function convertEnvInfo(env: PythonEnvInfo): Environment {
325325
if (convertedEnv.executable.sysPrefix === '') {
326326
convertedEnv.executable.sysPrefix = undefined;
327327
}
328-
if (convertedEnv.executable.uri?.fsPath === 'python') {
329-
convertedEnv.executable.uri = undefined;
328+
if (convertedEnv.version?.sysVersion === '') {
329+
convertedEnv.version.sysVersion = undefined;
330330
}
331-
if (convertedEnv.environment?.name === '') {
332-
convertedEnv.environment.name = undefined;
333-
}
334-
if (convertedEnv.version.major === -1) {
331+
if (convertedEnv.version?.major === -1) {
335332
convertedEnv.version.major = undefined;
336333
}
337-
if (convertedEnv.version.micro === -1) {
334+
if (convertedEnv.version?.micro === -1) {
338335
convertedEnv.version.micro = undefined;
339336
}
340-
if (convertedEnv.version.minor === -1) {
337+
if (convertedEnv.version?.minor === -1) {
341338
convertedEnv.version.minor = undefined;
342339
}
343340
return convertedEnv as Environment;

src/client/proposedApiTypes.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,16 @@ export type Environment = EnvironmentPath & {
129129
}
130130
| undefined;
131131
/**
132-
* Carries Python version information known at this moment.
132+
* Carries Python version information known at this moment, carries `undefined` for envs without python.
133133
*/
134-
readonly version: VersionInfo & {
135-
/**
136-
* Value of `sys.version` in sys module if known at this moment.
137-
*/
138-
readonly sysVersion: string | undefined;
139-
};
134+
readonly version:
135+
| (VersionInfo & {
136+
/**
137+
* Value of `sys.version` in sys module if known at this moment.
138+
*/
139+
readonly sysVersion: string | undefined;
140+
})
141+
| undefined;
140142
/**
141143
* Tools/plugins which created the environment or where it came from. First value in array corresponds
142144
* to the primary tool which manages the environment, which never changes over time.
@@ -171,14 +173,16 @@ export type ResolvedEnvironment = Environment & {
171173
readonly sysPrefix: string;
172174
};
173175
/**
174-
* Carries complete Python version information.
176+
* Carries complete Python version information, carries `undefined` for envs without python.
175177
*/
176-
readonly version: ResolvedVersionInfo & {
177-
/**
178-
* Value of `sys.version` in sys module if known at this moment.
179-
*/
180-
readonly sysVersion: string;
181-
};
178+
readonly version:
179+
| (ResolvedVersionInfo & {
180+
/**
181+
* Value of `sys.version` in sys module if known at this moment.
182+
*/
183+
readonly sysVersion: string;
184+
})
185+
| undefined;
182186
};
183187

184188
export type EnvironmentsChangeEvent = {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export class PythonEnvsResolver implements IResolvingLocator {
9595
} else if (seen[event.index] !== undefined) {
9696
const old = seen[event.index];
9797
await setKind(event.update, environmentKinds);
98-
seen[event.index] = await resolveBasicEnv(event.update, true);
98+
seen[event.index] = await resolveBasicEnv(event.update);
9999
didUpdate.fire({ old, index: event.index, update: seen[event.index] });
100100
this.resolveInBackground(event.index, state, didUpdate, seen).ignoreErrors();
101101
} else {
@@ -113,7 +113,7 @@ export class PythonEnvsResolver implements IResolvingLocator {
113113
while (!result.done) {
114114
// Use cache from the current refresh where possible.
115115
await setKind(result.value, environmentKinds);
116-
const currEnv = await resolveBasicEnv(result.value, true);
116+
const currEnv = await resolveBasicEnv(result.value);
117117
seen.push(currEnv);
118118
yield currEnv;
119119
this.resolveInBackground(seen.indexOf(currEnv), state, didUpdate, seen).ignoreErrors();

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

Lines changed: 36 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,8 @@ import {
1212
UNKNOWN_PYTHON_VERSION,
1313
virtualEnvKinds,
1414
} from '../../info';
15-
import {
16-
buildEnvInfo,
17-
comparePythonVersionSpecificity,
18-
setEnvDisplayString,
19-
areSameEnv,
20-
getEnvID,
21-
} from '../../info/env';
22-
import {
23-
getEnvironmentDirFromPath,
24-
getInterpreterPathFromDir,
25-
getPythonVersionFromPath,
26-
} from '../../../common/commonUtils';
15+
import { buildEnvInfo, comparePythonVersionSpecificity, setEnvDisplayString, getEnvID } from '../../info/env';
16+
import { getEnvironmentDirFromPath, getPythonVersionFromPath } from '../../../common/commonUtils';
2717
import { arePathsSame, getFileInfo, isParentPath } from '../../../common/externalDependencies';
2818
import { AnacondaCompanyName, Conda, isCondaEnvironment } from '../../../common/environmentManagers/conda';
2919
import { getPyenvVersionsDir, parsePyenvVersion } from '../../../common/environmentManagers/pyenv';
@@ -36,8 +26,8 @@ import { traceError, traceWarn } from '../../../../logging';
3626
import { isVirtualEnvironment } from '../../../common/environmentManagers/simplevirtualenvs';
3727
import { getWorkspaceFolderPaths } from '../../../../common/vscodeApis/workspaceApis';
3828

39-
function getResolvers(): Map<PythonEnvKind, (env: BasicEnvInfo, useCache?: boolean) => Promise<PythonEnvInfo>> {
40-
const resolvers = new Map<PythonEnvKind, (_: BasicEnvInfo, useCache?: boolean) => Promise<PythonEnvInfo>>();
29+
function getResolvers(): Map<PythonEnvKind, (env: BasicEnvInfo) => Promise<PythonEnvInfo>> {
30+
const resolvers = new Map<PythonEnvKind, (_: BasicEnvInfo) => Promise<PythonEnvInfo>>();
4131
Object.values(PythonEnvKind).forEach((k) => {
4232
resolvers.set(k, resolveGloballyInstalledEnv);
4333
});
@@ -55,11 +45,11 @@ function getResolvers(): Map<PythonEnvKind, (env: BasicEnvInfo, useCache?: boole
5545
* executable and returns it. Notice `undefined` is never returned, so environment
5646
* returned could still be invalid.
5747
*/
58-
export async function resolveBasicEnv(env: BasicEnvInfo, useCache = false): Promise<PythonEnvInfo> {
48+
export async function resolveBasicEnv(env: BasicEnvInfo): Promise<PythonEnvInfo> {
5949
const { kind, source } = env;
6050
const resolvers = getResolvers();
6151
const resolverForKind = resolvers.get(kind)!;
62-
const resolvedEnv = await resolverForKind(env, useCache);
52+
const resolvedEnv = await resolverForKind(env);
6353
resolvedEnv.searchLocation = getSearchLocation(resolvedEnv);
6454
resolvedEnv.source = uniq(resolvedEnv.source.concat(source ?? []));
6555
if (getOSType() === OSType.Windows && resolvedEnv.source?.includes(PythonEnvSource.WindowsRegistry)) {
@@ -165,47 +155,41 @@ async function resolveSimpleEnv(env: BasicEnvInfo): Promise<PythonEnvInfo> {
165155
return envInfo;
166156
}
167157

168-
async function resolveCondaEnv(env: BasicEnvInfo, useCache?: boolean): Promise<PythonEnvInfo> {
158+
async function resolveCondaEnv(env: BasicEnvInfo): Promise<PythonEnvInfo> {
169159
const { executablePath } = env;
170160
const conda = await Conda.getConda();
171161
if (conda === undefined) {
172-
traceWarn(`${executablePath} identified as Conda environment even though Conda is not installed`);
162+
traceWarn(`${executablePath} identified as Conda environment even though Conda is not found`);
163+
// Environment could still be valid, resolve as a simple env.
164+
env.kind = PythonEnvKind.Unknown;
165+
const envInfo = await resolveSimpleEnv(env);
166+
envInfo.type = PythonEnvType.Conda;
167+
// Assume it's a prefixed env by default because prefixed CLIs work even for named environments.
168+
envInfo.name = '';
169+
return envInfo;
173170
}
174-
const envs = (await conda?.getEnvList(useCache)) ?? [];
175-
for (const { name, prefix } of envs) {
176-
let executable = await getInterpreterPathFromDir(prefix);
177-
const currEnv: BasicEnvInfo = { executablePath: executable ?? '', kind: PythonEnvKind.Conda, envPath: prefix };
178-
if (areSameEnv(env, currEnv)) {
179-
if (env.executablePath.length > 0) {
180-
executable = env.executablePath;
181-
} else {
182-
executable = await conda?.getInterpreterPathForEnvironment({ name, prefix });
183-
}
184-
const info = buildEnvInfo({
185-
executable,
186-
kind: PythonEnvKind.Conda,
187-
org: AnacondaCompanyName,
188-
location: prefix,
189-
source: [],
190-
version: executable ? await getPythonVersionFromPath(executable) : undefined,
191-
type: PythonEnvType.Conda,
192-
});
193-
if (name) {
194-
info.name = name;
195-
}
196-
return info;
197-
}
171+
172+
const envPath = env.envPath ?? getEnvironmentDirFromPath(env.executablePath);
173+
let executable: string;
174+
if (env.executablePath.length > 0) {
175+
executable = env.executablePath;
176+
} else {
177+
executable = await conda.getInterpreterPathForEnvironment({ prefix: envPath });
198178
}
199-
traceError(
200-
`${env.envPath ?? env.executablePath} identified as a Conda environment but is not returned via '${
201-
conda?.command
202-
} info' command`,
203-
);
204-
// Environment could still be valid, resolve as a simple env.
205-
env.kind = PythonEnvKind.Unknown;
206-
const envInfo = await resolveSimpleEnv(env);
207-
envInfo.type = PythonEnvType.Conda;
208-
return envInfo;
179+
const info = buildEnvInfo({
180+
executable,
181+
kind: PythonEnvKind.Conda,
182+
org: AnacondaCompanyName,
183+
location: envPath,
184+
source: [],
185+
version: executable ? await getPythonVersionFromPath(executable) : undefined,
186+
type: PythonEnvType.Conda,
187+
});
188+
const name = await conda?.getName(envPath);
189+
if (name) {
190+
info.name = name;
191+
}
192+
return info;
209193
}
210194

211195
async function resolvePyenvEnv(env: BasicEnvInfo): Promise<PythonEnvInfo> {

src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,8 @@ export class CondaEnvironmentLocator extends FSWatchingLocator {
3232
try {
3333
traceVerbose(`Looking into conda env for executable: ${JSON.stringify(env)}`);
3434
const executablePath = await conda.getInterpreterPathForEnvironment(env);
35-
if (executablePath !== undefined) {
36-
traceVerbose(`Found conda executable: ${executablePath}`);
37-
yield { kind: PythonEnvKind.Conda, executablePath, envPath: env.prefix };
38-
} else {
39-
traceError(`Executable for conda env not found: ${JSON.stringify(env)}`);
40-
}
35+
traceVerbose(`Found conda executable: ${executablePath}`);
36+
yield { kind: PythonEnvKind.Conda, executablePath, envPath: env.prefix };
4137
} catch (ex) {
4238
traceError(`Failed to process conda env: ${JSON.stringify(env)}`, ex);
4339
}

src/client/pythonEnvironments/common/environmentManagers/conda.ts

Lines changed: 30 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -444,34 +444,34 @@ export class Conda {
444444
* Corresponds to "conda env list --json", but also computes environment names.
445445
*/
446446
@cache(30_000, true, 10_000)
447-
public async getEnvList(useCache?: boolean): Promise<CondaEnvInfo[]> {
448-
const info = await this.getInfo(useCache);
447+
public async getEnvList(): Promise<CondaEnvInfo[]> {
448+
const info = await this.getInfo();
449449
const { envs } = info;
450450
if (envs === undefined) {
451451
return [];
452452
}
453+
return Promise.all(
454+
envs.map(async (prefix) => ({
455+
prefix,
456+
name: await this.getName(prefix, info),
457+
})),
458+
);
459+
}
453460

454-
function getName(prefix: string) {
455-
if (info.root_prefix && arePathsSame(prefix, info.root_prefix)) {
456-
return 'base';
457-
}
458-
459-
const parentDir = path.dirname(prefix);
460-
if (info.envs_dirs !== undefined) {
461-
for (const envsDir of info.envs_dirs) {
462-
if (arePathsSame(parentDir, envsDir)) {
463-
return path.basename(prefix);
464-
}
461+
public async getName(prefix: string, info?: CondaInfo): Promise<string | undefined> {
462+
info = info ?? (await this.getInfo(true));
463+
if (info.root_prefix && arePathsSame(prefix, info.root_prefix)) {
464+
return 'base';
465+
}
466+
const parentDir = path.dirname(prefix);
467+
if (info.envs_dirs !== undefined) {
468+
for (const envsDir of info.envs_dirs) {
469+
if (arePathsSame(parentDir, envsDir)) {
470+
return path.basename(prefix);
465471
}
466472
}
467-
468-
return undefined;
469473
}
470-
471-
return envs.map((prefix) => ({
472-
prefix,
473-
name: getName(prefix),
474-
}));
474+
return undefined;
475475
}
476476

477477
/**
@@ -493,22 +493,17 @@ export class Conda {
493493
* Returns executable associated with the conda env, swallows exceptions.
494494
*/
495495
// eslint-disable-next-line class-methods-use-this
496-
public async getInterpreterPathForEnvironment(condaEnv: CondaEnvInfo): Promise<string | undefined> {
497-
try {
498-
const executablePath = await getInterpreterPath(condaEnv.prefix);
499-
if (executablePath) {
500-
traceVerbose('Found executable within conda env', JSON.stringify(condaEnv));
501-
return executablePath;
502-
}
503-
traceVerbose(
504-
'Executable does not exist within conda env, assume the executable to be `python`',
505-
JSON.stringify(condaEnv),
506-
);
507-
return 'python';
508-
} catch (ex) {
509-
traceError(`Failed to get executable for conda env: ${JSON.stringify(condaEnv)}`, ex);
510-
return undefined;
496+
public async getInterpreterPathForEnvironment(condaEnv: CondaEnvInfo | { prefix: string }): Promise<string> {
497+
const executablePath = await getInterpreterPath(condaEnv.prefix);
498+
if (executablePath) {
499+
traceVerbose('Found executable within conda env', JSON.stringify(condaEnv));
500+
return executablePath;
511501
}
502+
traceVerbose(
503+
'Executable does not exist within conda env, assume the executable to be `python`',
504+
JSON.stringify(condaEnv),
505+
);
506+
return 'python';
512507
}
513508

514509
public async getRunPythonArgs(env: CondaEnvInfo, forShellExecution?: boolean): Promise<string[] | undefined> {

0 commit comments

Comments
 (0)