Skip to content

Commit abe223f

Browse files
authored
Updates to use new Python locator Api (microsoft#23832)
1 parent 193b929 commit abe223f

File tree

7 files changed

+195
-160
lines changed

7 files changed

+195
-160
lines changed

src/client/common/utils/async.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,17 @@ export async function flattenIterator<T>(iterator: IAsyncIterator<T>): Promise<T
232232
return results;
233233
}
234234

235+
/**
236+
* Get everything yielded by the iterable.
237+
*/
238+
export async function flattenIterable<T>(iterableItem: AsyncIterable<T>): Promise<T[]> {
239+
const results: T[] = [];
240+
for await (const item of iterableItem) {
241+
results.push(item);
242+
}
243+
return results;
244+
}
245+
235246
/**
236247
* Wait for a condition to be fulfilled within a timeout.
237248
*

src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts

Lines changed: 111 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { getUserHomeDir } from '../../../../common/utils/platform';
1818
import { createLogOutputChannel } from '../../../../common/vscodeApis/windowApis';
1919
import { PythonEnvKind } from '../../info';
2020
import { sendNativeTelemetry, NativePythonTelemetry } from './nativePythonTelemetry';
21+
import { traceError } from '../../../../logging';
2122

2223
const untildify = require('untildify');
2324

@@ -29,7 +30,7 @@ export interface NativeEnvInfo {
2930
displayName?: string;
3031
name?: string;
3132
executable?: string;
32-
kind?: string;
33+
kind?: PythonEnvironmentKind;
3334
version?: string;
3435
prefix?: string;
3536
manager?: NativeEnvManagerInfo;
@@ -41,12 +42,38 @@ export interface NativeEnvInfo {
4142
symlinks?: string[];
4243
}
4344

45+
export enum PythonEnvironmentKind {
46+
Conda = 'Conda',
47+
Homebrew = 'Homebrew',
48+
Pyenv = 'Pyenv',
49+
GlobalPaths = 'GlobalPaths',
50+
PyenvVirtualEnv = 'PyenvVirtualEnv',
51+
Pipenv = 'Pipenv',
52+
Poetry = 'Poetry',
53+
MacPythonOrg = 'MacPythonOrg',
54+
MacCommandLineTools = 'MacCommandLineTools',
55+
LinuxGlobal = 'LinuxGlobal',
56+
MacXCode = 'MacXCode',
57+
Venv = 'Venv',
58+
VirtualEnv = 'VirtualEnv',
59+
VirtualEnvWrapper = 'VirtualEnvWrapper',
60+
WindowsStore = 'WindowsStore',
61+
WindowsRegistry = 'WindowsRegistry',
62+
}
63+
4464
export interface NativeEnvManagerInfo {
4565
tool: string;
4666
executable: string;
4767
version?: string;
4868
}
4969

70+
export function isNativeInfoEnvironment(info: NativeEnvInfo | NativeEnvManagerInfo): info is NativeEnvInfo {
71+
if ((info as NativeEnvManagerInfo).tool) {
72+
return false;
73+
}
74+
return true;
75+
}
76+
5077
export type NativeCondaInfo = {
5178
canSpawnConda: boolean;
5279
userProvidedEnvFound?: boolean;
@@ -58,12 +85,62 @@ export type NativeCondaInfo = {
5885
};
5986

6087
export interface NativePythonFinder extends Disposable {
88+
/**
89+
* Refresh the list of python environments.
90+
* Returns an async iterable that can be used to iterate over the list of python environments.
91+
* Internally this will take all of the current workspace folders and search for python environments.
92+
*
93+
* If a Uri is provided, then it will search for python environments in that location (ignoring workspaces).
94+
* Uri can be a file or a folder.
95+
* If a PythonEnvironmentKind is provided, then it will search for python environments of that kind (ignoring workspaces).
96+
*/
97+
refresh(options?: PythonEnvironmentKind | Uri[]): AsyncIterable<NativeEnvInfo | NativeEnvManagerInfo>;
98+
/**
99+
* Will spawn the provided Python executable and return information about the environment.
100+
* @param executable
101+
*/
61102
resolve(executable: string): Promise<NativeEnvInfo>;
62-
refresh(): AsyncIterable<NativeEnvInfo>;
63-
categoryToKind(category?: string): PythonEnvKind;
64-
logger(): LogOutputChannel;
103+
categoryToKind(category?: PythonEnvironmentKind): PythonEnvKind;
104+
/**
105+
* Used only for telemetry.
106+
*/
65107
getCondaInfo(): Promise<NativeCondaInfo>;
66-
find(searchPath: string): Promise<NativeEnvInfo[]>;
108+
}
109+
110+
const mapping = new Map<PythonEnvironmentKind, PythonEnvKind>([
111+
[PythonEnvironmentKind.Conda, PythonEnvKind.Conda],
112+
[PythonEnvironmentKind.GlobalPaths, PythonEnvKind.OtherGlobal],
113+
[PythonEnvironmentKind.Pyenv, PythonEnvKind.Pyenv],
114+
[PythonEnvironmentKind.PyenvVirtualEnv, PythonEnvKind.Pyenv],
115+
[PythonEnvironmentKind.Pipenv, PythonEnvKind.Pipenv],
116+
[PythonEnvironmentKind.Poetry, PythonEnvKind.Poetry],
117+
[PythonEnvironmentKind.VirtualEnv, PythonEnvKind.VirtualEnv],
118+
[PythonEnvironmentKind.VirtualEnvWrapper, PythonEnvKind.VirtualEnvWrapper],
119+
[PythonEnvironmentKind.Venv, PythonEnvKind.Venv],
120+
[PythonEnvironmentKind.WindowsRegistry, PythonEnvKind.System],
121+
[PythonEnvironmentKind.WindowsStore, PythonEnvKind.MicrosoftStore],
122+
[PythonEnvironmentKind.Homebrew, PythonEnvKind.System],
123+
[PythonEnvironmentKind.LinuxGlobal, PythonEnvKind.System],
124+
[PythonEnvironmentKind.MacCommandLineTools, PythonEnvKind.System],
125+
[PythonEnvironmentKind.MacPythonOrg, PythonEnvKind.System],
126+
[PythonEnvironmentKind.MacXCode, PythonEnvKind.System],
127+
]);
128+
129+
export function categoryToKind(category?: PythonEnvironmentKind, logger?: LogOutputChannel): PythonEnvKind {
130+
if (!category) {
131+
return PythonEnvKind.Unknown;
132+
}
133+
const kind = mapping.get(category);
134+
if (kind) {
135+
return kind;
136+
}
137+
138+
if (logger) {
139+
logger.error(`Unknown Python Environment category '${category}' from Native Locator.`);
140+
} else {
141+
traceError(`Unknown Python Environment category '${category}' from Native Locator.`);
142+
}
143+
return PythonEnvKind.Unknown;
67144
}
68145

69146
interface NativeLog {
@@ -94,47 +171,11 @@ class NativeGlobalPythonFinderImpl extends DisposableBase implements NativePytho
94171
return environment;
95172
}
96173

97-
categoryToKind(category?: string): PythonEnvKind {
98-
if (!category) {
99-
return PythonEnvKind.Unknown;
100-
}
101-
switch (category.toLowerCase()) {
102-
case 'conda':
103-
return PythonEnvKind.Conda;
104-
case 'system':
105-
case 'homebrew':
106-
case 'macpythonorg':
107-
case 'maccommandlinetools':
108-
case 'macxcode':
109-
case 'windowsregistry':
110-
case 'linuxglobal':
111-
return PythonEnvKind.System;
112-
case 'globalpaths':
113-
return PythonEnvKind.OtherGlobal;
114-
case 'pyenv':
115-
return PythonEnvKind.Pyenv;
116-
case 'poetry':
117-
return PythonEnvKind.Poetry;
118-
case 'pipenv':
119-
return PythonEnvKind.Pipenv;
120-
case 'pyenvvirtualenv':
121-
return PythonEnvKind.VirtualEnv;
122-
case 'venv':
123-
return PythonEnvKind.Venv;
124-
case 'virtualenv':
125-
return PythonEnvKind.VirtualEnv;
126-
case 'virtualenvwrapper':
127-
return PythonEnvKind.VirtualEnvWrapper;
128-
case 'windowsstore':
129-
return PythonEnvKind.MicrosoftStore;
130-
default: {
131-
this.outputChannel.info(`Unknown Python Environment category '${category}' from Native Locator.`);
132-
return PythonEnvKind.Unknown;
133-
}
134-
}
174+
categoryToKind(category?: PythonEnvironmentKind): PythonEnvKind {
175+
return categoryToKind(category, this.outputChannel);
135176
}
136177

137-
async *refresh(): AsyncIterable<NativeEnvInfo> {
178+
async *refresh(options?: PythonEnvironmentKind | Uri[]): AsyncIterable<NativeEnvInfo> {
138179
if (this.firstRefreshResults) {
139180
// If this is the first time we are refreshing,
140181
// Then get the results from the first refresh.
@@ -143,12 +184,12 @@ class NativeGlobalPythonFinderImpl extends DisposableBase implements NativePytho
143184
this.firstRefreshResults = undefined;
144185
yield* results;
145186
} else {
146-
const result = this.doRefresh();
187+
const result = this.doRefresh(options);
147188
let completed = false;
148189
void result.completed.finally(() => {
149190
completed = true;
150191
});
151-
const envs: NativeEnvInfo[] = [];
192+
const envs: (NativeEnvInfo | NativeEnvManagerInfo)[] = [];
152193
let discovered = createDeferred();
153194
const disposable = result.discovered((data) => {
154195
envs.push(data);
@@ -173,10 +214,6 @@ class NativeGlobalPythonFinderImpl extends DisposableBase implements NativePytho
173214
}
174215
}
175216

176-
logger(): LogOutputChannel {
177-
return this.outputChannel;
178-
}
179-
180217
refreshFirstTime() {
181218
const result = this.doRefresh();
182219
const completed = createDeferredFrom(result.completed);
@@ -283,9 +320,11 @@ class NativeGlobalPythonFinderImpl extends DisposableBase implements NativePytho
283320
return connection;
284321
}
285322

286-
private doRefresh(): { completed: Promise<void>; discovered: Event<NativeEnvInfo> } {
323+
private doRefresh(
324+
options?: PythonEnvironmentKind | Uri[],
325+
): { completed: Promise<void>; discovered: Event<NativeEnvInfo | NativeEnvManagerInfo> } {
287326
const disposable = this._register(new DisposableStore());
288-
const discovered = disposable.add(new EventEmitter<NativeEnvInfo>());
327+
const discovered = disposable.add(new EventEmitter<NativeEnvInfo | NativeEnvManagerInfo>());
289328
const completed = createDeferred<void>();
290329
const pendingPromises: Promise<void>[] = [];
291330

@@ -306,6 +345,8 @@ class NativeGlobalPythonFinderImpl extends DisposableBase implements NativePytho
306345
notifyUponCompletion();
307346
};
308347

348+
// Assumption is server will ensure there's only one refresh at a time.
349+
// Perhaps we should have a request Id or the like to map the results back to the `refresh` request.
309350
disposable.add(
310351
this.connection.onNotification('environment', (data: NativeEnvInfo) => {
311352
this.outputChannel.info(`Discovered env: ${data.executable || data.prefix}`);
@@ -334,11 +375,28 @@ class NativeGlobalPythonFinderImpl extends DisposableBase implements NativePytho
334375
}
335376
}),
336377
);
378+
disposable.add(
379+
this.connection.onNotification('manager', (data: NativeEnvManagerInfo) => {
380+
this.outputChannel.info(`Discovered manager: (${data.tool}) ${data.executable}`);
381+
discovered.fire(data);
382+
}),
383+
);
337384

385+
type RefreshOptions = {
386+
searchKind?: PythonEnvironmentKind;
387+
searchPaths?: string[];
388+
};
389+
390+
const refreshOptions: RefreshOptions = {};
391+
if (options && Array.isArray(options) && options.length > 0) {
392+
refreshOptions.searchPaths = options.map((item) => item.fsPath);
393+
} else if (options && typeof options === 'string') {
394+
refreshOptions.searchKind = options;
395+
}
338396
trackPromiseAndNotifyOnCompletion(
339397
this.configure().then(() =>
340398
this.connection
341-
.sendRequest<{ duration: number }>('refresh')
399+
.sendRequest<{ duration: number }>('refresh', refreshOptions)
342400
.then(({ duration }) => this.outputChannel.info(`Refresh completed in ${duration}ms`))
343401
.catch((ex) => this.outputChannel.error('Refresh error', ex)),
344402
),

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

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
// Licensed under the MIT License.
33

44
import * as fsPath from 'path';
5-
import { Event, EventEmitter, workspace } from 'vscode';
5+
import { Event, EventEmitter, Uri, workspace } from 'vscode';
66
import '../../../../common/extensions';
7-
import { createDeferred, Deferred } from '../../../../common/utils/async';
7+
import { createDeferred, Deferred, flattenIterable } from '../../../../common/utils/async';
88
import { StopWatch } from '../../../../common/utils/stopWatch';
99
import { traceError, traceInfo, traceVerbose } from '../../../../logging';
1010
import { sendTelemetryEvent } from '../../../../telemetry';
@@ -25,7 +25,12 @@ import {
2525
import { getQueryFilter } from '../../locatorUtils';
2626
import { PythonEnvCollectionChangedEvent, PythonEnvsWatcher } from '../../watcher';
2727
import { IEnvsCollectionCache } from './envsCollectionCache';
28-
import { getNativePythonFinder, NativeEnvInfo, NativePythonFinder } from '../common/nativePythonFinder';
28+
import {
29+
getNativePythonFinder,
30+
isNativeInfoEnvironment,
31+
NativeEnvInfo,
32+
NativePythonFinder,
33+
} from '../common/nativePythonFinder';
2934
import { pathExists } from '../../../../common/platform/fs-paths';
3035
import { noop } from '../../../../common/utils/misc';
3136
import { parseVersion } from '../../info/pythonVersion';
@@ -294,16 +299,18 @@ export class EnvsCollectionService extends PythonEnvsWatcher<PythonEnvCollection
294299
const executablesFoundByNativeLocator = new Set<string>();
295300
const nativeStopWatch = new StopWatch();
296301
for await (const data of this.nativeFinder.refresh()) {
297-
nativeEnvs.push(data);
298-
if (data.executable) {
299-
// Lowercase for purposes of comparison (safe).
300-
executablesFoundByNativeLocator.add(data.executable.toLowerCase());
301-
} else if (data.prefix) {
302+
if (isNativeInfoEnvironment(data)) {
303+
nativeEnvs.push(data);
304+
if (data.executable) {
305+
// Lowercase for purposes of comparison (safe).
306+
executablesFoundByNativeLocator.add(data.executable.toLowerCase());
307+
} else if (data.prefix) {
308+
// Lowercase for purposes of comparison (safe).
309+
executablesFoundByNativeLocator.add(data.prefix.toLowerCase());
310+
}
302311
// Lowercase for purposes of comparison (safe).
303-
executablesFoundByNativeLocator.add(data.prefix.toLowerCase());
312+
(data.symlinks || []).forEach((exe) => executablesFoundByNativeLocator.add(exe.toLowerCase()));
304313
}
305-
// Lowercase for purposes of comparison (safe).
306-
(data.symlinks || []).forEach((exe) => executablesFoundByNativeLocator.add(exe.toLowerCase()));
307314
}
308315
const nativeDuration = nativeStopWatch.elapsedTime;
309316
void this.sendNativeLocatorTelemetry(nativeEnvs);
@@ -980,11 +987,11 @@ async function getCondaTelemetry(
980987

981988
if (condaTelemetry.condaRootPrefixFoundInInfoNotInNative) {
982989
// Verify we are able to discover this environment as a conda env using native finder.
983-
const rootPrefixEnvs = await nativeFinder.find(rootPrefix);
990+
const rootPrefixEnvs = await flattenIterable(nativeFinder.refresh([Uri.file(rootPrefix)]));
984991
// Did we find an env with the same prefix?
985-
const rootPrefixEnv = rootPrefixEnvs.find(
986-
(e) => fsPath.normalize(e.prefix || '').toLowerCase() === rootPrefix.toLowerCase(),
987-
);
992+
const rootPrefixEnv = rootPrefixEnvs
993+
.filter(isNativeInfoEnvironment)
994+
.find((e) => fsPath.normalize(e.prefix || '').toLowerCase() === rootPrefix.toLowerCase());
988995
condaTelemetry.condaRootPrefixEnvsAfterFind = rootPrefixEnvs.length;
989996
condaTelemetry.condaRootPrefixFoundInInfoAfterFind = !!rootPrefixEnv;
990997
condaTelemetry.condaRootPrefixFoundInInfoAfterFindKind = rootPrefixEnv?.kind;
@@ -1019,11 +1026,11 @@ async function getCondaTelemetry(
10191026

10201027
if (condaTelemetry.condaDefaultPrefixFoundInInfoNotInNative) {
10211028
// Verify we are able to discover this environment as a conda env using native finder.
1022-
const defaultPrefixEnvs = await nativeFinder.find(defaultPrefix);
1029+
const defaultPrefixEnvs = await flattenIterable(nativeFinder.refresh([Uri.file(defaultPrefix)]));
10231030
// Did we find an env with the same prefix?
1024-
const defaultPrefixEnv = defaultPrefixEnvs.find(
1025-
(e) => fsPath.normalize(e.prefix || '').toLowerCase() === defaultPrefix.toLowerCase(),
1026-
);
1031+
const defaultPrefixEnv = defaultPrefixEnvs
1032+
.filter(isNativeInfoEnvironment)
1033+
.find((e) => fsPath.normalize(e.prefix || '').toLowerCase() === defaultPrefix.toLowerCase());
10271034
condaTelemetry.condaDefaultPrefixEnvsAfterFind = defaultPrefixEnvs.length;
10281035
condaTelemetry.condaDefaultPrefixFoundInInfoAfterFind = !!defaultPrefixEnv;
10291036
condaTelemetry.condaDefaultPrefixFoundInInfoAfterFindKind = defaultPrefixEnv?.kind;

0 commit comments

Comments
 (0)