Skip to content

Commit 329292c

Browse files
author
Eric Snow
authored
Add the base Python envs locators. (microsoft#13764)
1 parent 37ff6cb commit 329292c

File tree

9 files changed

+789
-141
lines changed

9 files changed

+789
-141
lines changed

src/client/common/utils/async.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -117,16 +117,16 @@ export function createDeferredFromPromise<T>(promise: Promise<T>): Deferred<T> {
117117
/**
118118
* An iterator that yields nothing.
119119
*/
120-
export function iterEmpty<T, R = void>(): AsyncIterator<T, R> {
120+
export function iterEmpty<T>(): AsyncIterator<T, void> {
121121
// tslint:disable-next-line:no-empty
122-
return ((async function* () {})() as unknown) as AsyncIterator<T, R>;
122+
return ((async function* () {})() as unknown) as AsyncIterator<T, void>;
123123
}
124124

125-
type NextResult<T, R = void> = { index: number } & (
126-
| { result: IteratorResult<T, R>; err: null }
125+
type NextResult<T> = { index: number } & (
126+
| { result: IteratorResult<T, T | void>; err: null }
127127
| { result: null; err: Error }
128128
);
129-
async function getNext<T, R = void>(it: AsyncIterator<T, R>, indexMaybe?: number): Promise<NextResult<T, R>> {
129+
async function getNext<T>(it: AsyncIterator<T, T | void>, indexMaybe?: number): Promise<NextResult<T>> {
130130
const index = indexMaybe === undefined ? -1 : indexMaybe;
131131
try {
132132
const result = await it.next();
@@ -149,32 +149,34 @@ const NEVER: Promise<unknown> = new Promise(() => {});
149149
* @param iterators - the async iterators from which to yield items
150150
* @param onError - called/awaited once for each iterator that fails
151151
*/
152-
export async function* chain<T, R = void>(
153-
iterators: AsyncIterator<T | void, R>[],
152+
export async function* chain<T>(
153+
iterators: AsyncIterator<T, T | void>[],
154154
onError?: (err: Error, index: number) => Promise<void>
155155
// Ultimately we may also want to support cancellation.
156-
): AsyncIterator<T | R | void, void> {
156+
): AsyncIterator<T, void> {
157157
const promises = iterators.map(getNext);
158158
let numRunning = iterators.length;
159159
while (numRunning > 0) {
160160
const { index, result, err } = await Promise.race(promises);
161161
if (err !== null) {
162-
promises[index] = NEVER as Promise<NextResult<T, R>>;
162+
promises[index] = NEVER as Promise<NextResult<T>>;
163163
numRunning -= 1;
164164
if (onError !== undefined) {
165165
await onError(err, index);
166166
}
167167
// XXX Log the error.
168168
} else if (result!.done) {
169-
promises[index] = NEVER as Promise<NextResult<T, R>>;
169+
promises[index] = NEVER as Promise<NextResult<T>>;
170170
numRunning -= 1;
171171
// If R is void then result.value will be undefined.
172172
if (result!.value !== undefined) {
173173
yield result!.value;
174174
}
175175
} else {
176176
promises[index] = getNext(iterators[index], index);
177-
yield result!.value;
177+
// Only the "return" result can be undefined (void),
178+
// so we're okay here.
179+
yield result!.value as T;
178180
}
179181
}
180182
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { Event, Uri } from 'vscode';
5+
import { iterEmpty } from '../../common/utils/async';
6+
import { PythonEnvInfo, PythonEnvKind } from './info';
7+
import { BasicPythonEnvsChangedEvent, IPythonEnvsWatcher, PythonEnvsChangedEvent, PythonEnvsWatcher } from './watcher';
8+
9+
/**
10+
* An async iterator of `PythonEnvInfo`.
11+
*/
12+
export type PythonEnvsIterator = AsyncIterator<PythonEnvInfo, void>;
13+
14+
/**
15+
* An empty Python envs iterator.
16+
*/
17+
export const NOOP_ITERATOR: PythonEnvsIterator = iterEmpty<PythonEnvInfo>();
18+
19+
/**
20+
* The most basic info to send to a locator when requesting environments.
21+
*
22+
* This is directly correlated with the `BasicPythonEnvsChangedEvent`
23+
* emitted by watchers.
24+
*
25+
* @prop kinds - if provided, results should be limited to these env kinds
26+
*/
27+
export type BasicPythonLocatorQuery = {
28+
kinds?: PythonEnvKind[];
29+
};
30+
31+
/**
32+
* The full set of possible info to send to a locator when requesting environments.
33+
*
34+
* This is directly correlated with the `PythonEnvsChangedEvent`
35+
* emitted by watchers.
36+
*
37+
* @prop - searchLocations - if provided, results should be limited to
38+
* within these locations
39+
*/
40+
export type PythonLocatorQuery = BasicPythonLocatorQuery & {
41+
searchLocations?: Uri[];
42+
};
43+
44+
type QueryForEvent<E> = E extends PythonEnvsChangedEvent ? PythonLocatorQuery : BasicPythonLocatorQuery;
45+
46+
/**
47+
* A single Python environment locator.
48+
*
49+
* Each locator object is responsible for identifying the Python
50+
* environments in a single location, whether a directory, a directory
51+
* tree, or otherwise. That location is identified when the locator
52+
* is instantiated.
53+
*
54+
* Based on the narrow focus of each locator, the assumption is that
55+
* calling iterEnvs() to pick up a changed env is effectively no more
56+
* expensive than tracking down that env specifically. Consequently,
57+
* events emitted via `onChanged` do not need to provide information
58+
* for the specific environments that changed.
59+
*/
60+
export interface ILocator<E extends BasicPythonEnvsChangedEvent = PythonEnvsChangedEvent>
61+
extends IPythonEnvsWatcher<E> {
62+
/**
63+
* Iterate over the enviroments known tos this locator.
64+
*
65+
* Locators are not required to have provide all info about
66+
* an environment. However, each yielded item will at least
67+
* include all the `PythonEnvBaseInfo` data.
68+
*
69+
* @param query - if provided, the locator will limit results to match
70+
*/
71+
iterEnvs(query?: QueryForEvent<E>): PythonEnvsIterator;
72+
73+
/**
74+
* Find the given Python environment and fill in as much missing info as possible.
75+
*
76+
* If the locator can find the environment then the result is as
77+
* much info about that env as the locator has. At the least this
78+
* will include all the `PythonEnvBaseInfo` data. If a `PythonEnvInfo`
79+
* was provided then the result will be a copy with any updates or
80+
* extra info applied.
81+
*
82+
* If the locator could not find the environment then `undefined`
83+
* is returned.
84+
*
85+
* @param env - the Python executable path or partial env info to find and update
86+
*/
87+
resolveEnv(env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined>;
88+
}
89+
90+
interface IEmitter<E extends BasicPythonEnvsChangedEvent> {
91+
fire(e: E): void;
92+
}
93+
94+
/**
95+
* The generic base for Python envs locators.
96+
*
97+
* By default `resolveEnv()` returns undefined. Subclasses may override
98+
* the method to provide an implementation.
99+
*
100+
* Subclasses will call `this.emitter.fire()` to emit events.
101+
*
102+
* Also, in most cases the default event type (`PythonEnvsChangedEvent`)
103+
* should be used. Only in low-level cases should you consider using
104+
* `BasicPythonEnvsChangedEvent`.
105+
*/
106+
export abstract class LocatorBase<E extends BasicPythonEnvsChangedEvent = PythonEnvsChangedEvent> implements ILocator<E> {
107+
public readonly onChanged: Event<E>;
108+
protected readonly emitter: IEmitter<E>;
109+
constructor(watcher: IPythonEnvsWatcher<E> & IEmitter<E>) {
110+
this.emitter = watcher;
111+
this.onChanged = watcher.onChanged;
112+
}
113+
114+
public abstract iterEnvs(query?: QueryForEvent<E>): PythonEnvsIterator;
115+
116+
public async resolveEnv(_env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
117+
return undefined;
118+
}
119+
}
120+
121+
/**
122+
* The base for most Python envs locators.
123+
*
124+
* By default `resolveEnv()` returns undefined. Subclasses may override
125+
* the method to provide an implementation.
126+
*
127+
* Subclasses will call `this.emitter.fire()` * to emit events.
128+
*
129+
* In most cases this is the class you will want to subclass.
130+
* Only in low-level cases should you consider subclassing `LocatorBase`
131+
* using `BasicPythonEnvsChangedEvent.
132+
*/
133+
export abstract class Locator extends LocatorBase {
134+
constructor() {
135+
super(new PythonEnvsWatcher());
136+
}
137+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { chain } from '../../common/utils/async';
5+
import { PythonEnvInfo } from './info';
6+
import { ILocator, NOOP_ITERATOR, PythonEnvsIterator, PythonLocatorQuery } from './locator';
7+
import { DisableableEnvsWatcher, PythonEnvsWatchers } from './watchers';
8+
9+
/**
10+
* A wrapper around a set of locators, exposing them as a single locator.
11+
*
12+
* Events and iterator results are combined.
13+
*/
14+
export class Locators extends PythonEnvsWatchers implements ILocator {
15+
constructor(
16+
// The locators will be watched as well as iterated.
17+
private readonly locators: ReadonlyArray<ILocator>
18+
) {
19+
super(locators);
20+
}
21+
22+
public iterEnvs(query?: PythonLocatorQuery): PythonEnvsIterator {
23+
const iterators = this.locators.map((loc) => loc.iterEnvs(query));
24+
return chain(iterators);
25+
}
26+
27+
public async resolveEnv(env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
28+
for (const locator of this.locators) {
29+
const resolved = await locator.resolveEnv(env);
30+
if (resolved !== undefined) {
31+
return resolved;
32+
}
33+
}
34+
return undefined;
35+
}
36+
}
37+
38+
/**
39+
* A locator wrapper that can be disabled.
40+
*
41+
* If disabled, events emitted by the wrapped locator are discarded,
42+
* `iterEnvs()` yields nothing, and `resolveEnv()` already returns
43+
* `undefined`.
44+
*/
45+
export class DisableableLocator extends DisableableEnvsWatcher implements ILocator {
46+
constructor(
47+
// To wrapp more than one use `Locators`.
48+
private readonly locator: ILocator
49+
) {
50+
super(locator);
51+
}
52+
53+
public iterEnvs(query?: PythonLocatorQuery): PythonEnvsIterator {
54+
if (!this.enabled) {
55+
return NOOP_ITERATOR;
56+
}
57+
return this.locator.iterEnvs(query);
58+
}
59+
60+
public async resolveEnv(env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
61+
if (!this.enabled) {
62+
return undefined;
63+
}
64+
return this.locator.resolveEnv(env);
65+
}
66+
}

src/client/pythonEnvironments/base/watcher.ts

Lines changed: 12 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
import { Event, EventEmitter, Uri } from 'vscode';
77
import { PythonEnvKind } from './info';
88

9+
// The use cases for `BasicPythonEnvsChangedEvent` are currently
10+
// hypothetical. However, there's a real chance they may prove
11+
// useful for the concrete low-level locators. So for now we are
12+
// keeping the separate "basic" type.
13+
914
/**
1015
* The most basic info for a Python environments event.
1116
*
@@ -40,17 +45,21 @@ export interface IPythonEnvsWatcher<E extends BasicPythonEnvsChangedEvent = Pyth
4045
}
4146

4247
/**
43-
* This provides the fundamental functionality of a watcher for any event type.
48+
* This provides the fundamental functionality of a Python envs watcher.
4449
*
4550
* Consumers register listeners (callbacks) using `onChanged`. Each
4651
* listener is invoked when `fire()` is called.
4752
*
48-
* Note that in most cases classes will not inherit from this classes,
53+
* Note that in most cases classes will not inherit from this class,
4954
* but instead keep a private watcher property. The rule of thumb
5055
* is to follow whether or not consumers of *that* class should be able
5156
* to trigger events (via `fire()`).
57+
*
58+
* Also, in most cases the default event type (`PythonEnvsChangedEvent`)
59+
* should be used. Only in low-level cases should you consider using
60+
* `BasicPythonEnvsChangedEvent`.
5261
*/
53-
class WatcherBase<T> implements IPythonEnvsWatcher<T> {
62+
export class PythonEnvsWatcher<T extends BasicPythonEnvsChangedEvent = PythonEnvsChangedEvent> implements IPythonEnvsWatcher<T> {
5463
/**
5564
* The hook for registering event listeners (callbacks).
5665
*/
@@ -68,49 +77,3 @@ class WatcherBase<T> implements IPythonEnvsWatcher<T> {
6877
this.didChange.fire(event);
6978
}
7079
}
71-
72-
// The use cases for BasicPythonEnvsWatcher are currently hypothetical.
73-
// However, there's a real chance they may prove useful for the concrete
74-
// locators. Adding BasicPythonEnvsWatcher later will be much harder
75-
// than removing it later, so we're leaving it for now.
76-
77-
/**
78-
* A watcher for the basic Python environments events.
79-
*
80-
* This should be used only in low-level cases, with the most
81-
* rudimentary watchers. Most of the time `PythonEnvsWatcher`
82-
* should be used instead.
83-
*
84-
* Note that in most cases classes will not inherit from this classes,
85-
* but instead keep a private watcher property. The rule of thumb
86-
* is to follow whether or not consumers of *that* class should be able
87-
* to trigger events (via `fire()`).
88-
*/
89-
export class BasicPythonEnvsWatcher extends WatcherBase<BasicPythonEnvsChangedEvent> {
90-
/**
91-
* Fire an event based on the given info.
92-
*/
93-
public trigger(kind?: PythonEnvKind) {
94-
this.fire({ kind });
95-
}
96-
}
97-
98-
/**
99-
* A general-use watcher for Python environments events.
100-
*
101-
* In most cases this is the class you will want to use or subclass.
102-
* Only in low-level cases should you consider using `BasicPythonEnvsWatcher`.
103-
*
104-
* Note that in most cases classes will not inherit from this classes,
105-
* but instead keep a private watcher property. The rule of thumb
106-
* is to follow whether or not consumers of *that* class should be able
107-
* to trigger events (via `fire()`).
108-
*/
109-
export class PythonEnvsWatcher extends WatcherBase<PythonEnvsChangedEvent> {
110-
/**
111-
* Fire an event based on the given info.
112-
*/
113-
public trigger(kind?: PythonEnvKind, searchLocation?: Uri) {
114-
this.fire({ kind, searchLocation });
115-
}
116-
}

src/client/pythonEnvironments/base/watchers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ type EnvsEventListener = (e: PythonEnvsChangedEvent) => unknown;
3131
* If disabled, events emitted by the wrapped watcher are discarded.
3232
*/
3333
export class DisableableEnvsWatcher implements IPythonEnvsWatcher {
34-
private enabled = true;
34+
protected enabled = true;
3535
constructor(
3636
// To wrap more than one use `PythonEnvWatchers`.
3737
private readonly wrapped: IPythonEnvsWatcher

0 commit comments

Comments
 (0)