Skip to content

Commit b6ee6c6

Browse files
Add an extension-specific locator that will wrap the low-level locators. (#13780)
1 parent 6741162 commit b6ee6c6

File tree

4 files changed

+1002
-12
lines changed

4 files changed

+1002
-12
lines changed

src/client/common/utils/async.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ async function getNext<T>(it: AsyncIterator<T, T | void>, indexMaybe?: number):
137137
}
138138

139139
// tslint:disable-next-line:promise-must-complete no-empty
140-
const NEVER: Promise<unknown> = new Promise(() => {});
140+
export const NEVER: Promise<unknown> = new Promise(() => {});
141141

142142
/**
143143
* Yield everything produced by the given iterators as soon as each is ready.
@@ -181,6 +181,43 @@ export async function* chain<T>(
181181
}
182182
}
183183

184+
/**
185+
* Map the async function onto the items and yield the results.
186+
*
187+
* @param items - the items to map onto and iterate
188+
* @param func - the async function to apply for each item
189+
* @param race - if `true` (the default) then results are yielded
190+
* potentially out of order, as soon as each is ready
191+
*/
192+
export async function* mapToIterator<T, R = T>(
193+
items: T[],
194+
func: (item: T) => Promise<R>,
195+
race = true
196+
): AsyncIterator<R, void> {
197+
if (race) {
198+
const iterators = items.map((item) => {
199+
async function* generator() {
200+
yield func(item);
201+
}
202+
return generator();
203+
});
204+
yield* iterable(chain(iterators));
205+
} else {
206+
yield* items.map(func);
207+
}
208+
}
209+
210+
/**
211+
* Convert an iterator into an iterable, if it isn't one already.
212+
*/
213+
export function iterable<T>(iterator: AsyncIterator<T, void>): AsyncIterableIterator<T> {
214+
const it = iterator as AsyncIterableIterator<T>;
215+
if (it[Symbol.asyncIterator] === undefined) {
216+
it[Symbol.asyncIterator] = () => it;
217+
}
218+
return it;
219+
}
220+
184221
/**
185222
* Get everything yielded by the iterator.
186223
*/

src/client/pythonEnvironments/discovery/locators/index.ts

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
import { traceDecorators } from '../../../common/logger';
66
import { IPlatformService } from '../../../common/platform/types';
77
import { IDisposableRegistry } from '../../../common/types';
8-
import { createDeferred, Deferred } from '../../../common/utils/async';
8+
import { chain, createDeferred, Deferred } from '../../../common/utils/async';
99
import { OSType } from '../../../common/utils/platform';
1010
import {
1111
CONDA_ENV_FILE_SERVICE,
@@ -20,13 +20,157 @@ import {
2020
WORKSPACE_VIRTUAL_ENV_SERVICE,
2121
} from '../../../interpreter/contracts';
2222
import { IServiceContainer } from '../../../ioc/types';
23+
import { PythonEnvInfo } from '../../base/info';
24+
import { ILocator, Locator, NOOP_ITERATOR, PythonEnvsIterator, PythonLocatorQuery } from '../../base/locator';
25+
import { DisableableLocator, Locators } from '../../base/locators';
2326
import { PythonEnvironment } from '../../info';
2427
import { isHiddenInterpreter } from './services/interpreterFilter';
2528
import { GetInterpreterLocatorOptions } from './types';
2629

2730
// tslint:disable-next-line:no-require-imports no-var-requires
2831
const flatten = require('lodash/flatten') as typeof import('lodash/flatten');
2932

33+
/**
34+
* A wrapper around all locators used by the extension.
35+
*/
36+
export class ExtensionLocators extends Locators {
37+
constructor(
38+
// These are expected to be low-level locators (e.g. system).
39+
nonWorkspace: ILocator[],
40+
// This is expected to be a locator wrapping any found in
41+
// the workspace (i.e. WorkspaceLocators).
42+
workspace: ILocator
43+
) {
44+
super([...nonWorkspace, workspace]);
45+
}
46+
}
47+
48+
type WorkspaceLocatorFactory = (root: Uri) => ILocator[];
49+
50+
interface IWorkspaceFolders {
51+
readonly roots: ReadonlyArray<Uri>;
52+
readonly onAdded: Event<Uri>;
53+
readonly onRemoved: Event<Uri>;
54+
}
55+
56+
type RootURI = string;
57+
58+
/**
59+
* The collection of all workspace-specific locators used by the extension.
60+
*
61+
* The factories are used to produce the locators for each workspace folder.
62+
*/
63+
export class WorkspaceLocators extends Locator {
64+
private readonly locators: Record<RootURI, DisableableLocator> = {};
65+
private readonly roots: Record<RootURI, Uri> = {};
66+
constructor(
67+
// used to produce the per-root locators:
68+
private readonly factories: WorkspaceLocatorFactory[]
69+
) {
70+
super();
71+
}
72+
73+
/**
74+
* Activate the locator.
75+
*
76+
* @param folders - the info used to keep track of the workspace folders
77+
*/
78+
public activate(folders: IWorkspaceFolders) {
79+
for (const root of folders.roots) {
80+
this.addRoot(root);
81+
}
82+
folders.onAdded((root: Uri) => this.addRoot(root));
83+
folders.onRemoved((root: Uri) => this.removeRoot(root));
84+
}
85+
86+
public iterEnvs(query?: PythonLocatorQuery): PythonEnvsIterator {
87+
const iterators = Object.keys(this.locators).map((key) => {
88+
if (query?.searchLocations) {
89+
const root = this.roots[key];
90+
if (!matchURI(root, ...query.searchLocations)) {
91+
return NOOP_ITERATOR;
92+
}
93+
}
94+
// The query matches or was not location-specific.
95+
const locator = this.locators[key];
96+
return locator.iterEnvs(query);
97+
});
98+
return chain(iterators);
99+
}
100+
101+
public async resolveEnv(env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
102+
if (typeof env !== 'string' && env.searchLocation) {
103+
const rootLocator = this.locators[env.searchLocation.toString()];
104+
if (rootLocator) {
105+
return rootLocator.resolveEnv(env);
106+
}
107+
}
108+
// Fall back to checking all the roots.
109+
for (const key of Object.keys(this.locators)) {
110+
const resolved = await this.locators[key].resolveEnv(env);
111+
if (resolved !== undefined) {
112+
return resolved;
113+
}
114+
}
115+
return undefined;
116+
}
117+
118+
private addRoot(root: Uri) {
119+
// Drop the old one, if necessary.
120+
this.removeRoot(root);
121+
// Create the root's locator, wrapping each factory-generated locator.
122+
const locators: ILocator[] = [];
123+
for (const create of this.factories) {
124+
locators.push(...create(root));
125+
}
126+
const locator = new DisableableLocator(new Locators(locators));
127+
// Cache it.
128+
const key = root.toString();
129+
this.locators[key] = locator;
130+
this.roots[key] = root;
131+
this.emitter.fire({ searchLocation: root });
132+
// Hook up the watchers.
133+
locator.onChanged((e) => {
134+
if (e.searchLocation === undefined) {
135+
e.searchLocation = root;
136+
}
137+
this.emitter.fire(e);
138+
});
139+
}
140+
141+
private removeRoot(root: Uri) {
142+
const key = root.toString();
143+
const locator = this.locators[key];
144+
if (locator === undefined) {
145+
return;
146+
}
147+
delete this.locators[key];
148+
delete this.roots[key];
149+
locator.disable();
150+
this.emitter.fire({ searchLocation: root });
151+
}
152+
}
153+
154+
/**
155+
* Determine if the given URI matches one of the candidates.
156+
*
157+
* The scheme must match, as well as path. The path must match exactly
158+
* or the URI must be a parent of one of the candidates.
159+
*/
160+
function matchURI(uri: Uri, ...candidates: Uri[]): boolean {
161+
const uriPath = uri.path.endsWith('/') ? uri.path : `{uri.path}/`;
162+
for (const candidate of candidates) {
163+
if (candidate.scheme === uri.scheme) {
164+
if (candidate.path === uri.path) {
165+
return true;
166+
} else if (candidate.path.startsWith(uriPath)) {
167+
return true;
168+
}
169+
}
170+
}
171+
return false;
172+
}
173+
30174
/**
31175
* Facilitates locating Python interpreters.
32176
*/

src/test/pythonEnvironments/base/common.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
import { createDeferred, flattenIterator } from '../../../client/common/utils/async';
4+
import { createDeferred, flattenIterator, iterable, mapToIterator } from '../../../client/common/utils/async';
55
import { Architecture } from '../../../client/common/utils/platform';
66
import { EMPTY_VERSION, parseBasicVersionInfo } from '../../../client/common/utils/version';
77
import {
@@ -122,14 +122,24 @@ export class SimpleLocator extends Locator {
122122
if (callbacks?.before !== undefined) {
123123
await callbacks.before;
124124
}
125-
//yield* envs;
126-
for (const env of envs) {
127-
if (callbacks?.beforeEach !== undefined) {
128-
await callbacks.beforeEach(env);
125+
if (callbacks?.beforeEach !== undefined) {
126+
// The results will likely come in a different order.
127+
const mapped = mapToIterator(envs, async (env) => {
128+
await callbacks.beforeEach!(env);
129+
return env;
130+
});
131+
for await (const env of iterable(mapped)) {
132+
yield env;
133+
if (callbacks?.afterEach !== undefined) {
134+
await callbacks.afterEach(env);
135+
}
129136
}
130-
yield env;
131-
if (callbacks?.afterEach !== undefined) {
132-
await callbacks.afterEach(env);
137+
} else {
138+
for (const env of envs) {
139+
yield env;
140+
if (callbacks?.afterEach !== undefined) {
141+
await callbacks.afterEach(env);
142+
}
133143
}
134144
}
135145
if (callbacks?.after!== undefined) {

0 commit comments

Comments
 (0)