Skip to content

Commit 6a3db71

Browse files
author
Eric Snow
authored
Add the base Python environments "watchers". (microsoft#13735)
Watchers are the event-based part of locators.
1 parent 6b2fd32 commit 6a3db71

File tree

4 files changed

+469
-0
lines changed

4 files changed

+469
-0
lines changed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
// tslint:disable:max-classes-per-file
5+
6+
import { Event, EventEmitter, Uri } from 'vscode';
7+
import { PythonEnvKind } from './info';
8+
9+
/**
10+
* The most basic info for a Python environments event.
11+
*
12+
* @prop kind - the env kind, if any, affected by the event
13+
*/
14+
export type BasicPythonEnvsChangedEvent = {
15+
kind?: PythonEnvKind;
16+
};
17+
18+
/**
19+
* The full set of possible info for a Python environments event.
20+
*
21+
* @prop searchLocation - the location, if any, affected by the event
22+
*/
23+
export type PythonEnvsChangedEvent = BasicPythonEnvsChangedEvent & {
24+
searchLocation?: Uri;
25+
};
26+
27+
/**
28+
* A "watcher" for events related to changes to Python environemts.
29+
*
30+
* The watcher will notify listeners (callbacks registered through
31+
* `onChanged`) of events at undetermined times. The actual emitted
32+
* events, their source, and the timing is entirely up to the watcher
33+
* implementation.
34+
*/
35+
export interface IPythonEnvsWatcher<E extends BasicPythonEnvsChangedEvent = PythonEnvsChangedEvent> {
36+
/**
37+
* The hook for registering event listeners (callbacks).
38+
*/
39+
readonly onChanged: Event<E>;
40+
}
41+
42+
/**
43+
* This provides the fundamental functionality of a watcher for any event type.
44+
*
45+
* Consumers register listeners (callbacks) using `onChanged`. Each
46+
* listener is invoked when `fire()` is called.
47+
*
48+
* Note that in most cases classes will not inherit from this classes,
49+
* but instead keep a private watcher property. The rule of thumb
50+
* is to follow whether or not consumers of *that* class should be able
51+
* to trigger events (via `fire()`).
52+
*/
53+
class WatcherBase<T> implements IPythonEnvsWatcher<T> {
54+
/**
55+
* The hook for registering event listeners (callbacks).
56+
*/
57+
public readonly onChanged: Event<T>;
58+
private readonly didChange = new EventEmitter<T>();
59+
60+
constructor() {
61+
this.onChanged = this.didChange.event;
62+
}
63+
64+
/**
65+
* Send the event to all registered listeners.
66+
*/
67+
public fire(event: T) {
68+
this.didChange.fire(event);
69+
}
70+
}
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+
}
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 { Disposable, Event } from 'vscode';
5+
import { IPythonEnvsWatcher, PythonEnvsChangedEvent, PythonEnvsWatcher } from './watcher';
6+
7+
/**
8+
* A wrapper around a set of watchers, exposing them as a single watcher.
9+
*
10+
* If any of the wrapped watchers emits an event then this wrapper
11+
* emits that event.
12+
*/
13+
export class PythonEnvsWatchers implements IPythonEnvsWatcher {
14+
public readonly onChanged: Event<PythonEnvsChangedEvent>;
15+
private watcher = new PythonEnvsWatcher();
16+
17+
constructor(watchers: ReadonlyArray<IPythonEnvsWatcher>) {
18+
this.onChanged = this.watcher.onChanged;
19+
watchers.forEach((w) => {
20+
w.onChanged((e) => this.watcher.fire(e));
21+
});
22+
}
23+
}
24+
25+
// This matches the `vscode.Event` arg.
26+
type EnvsEventListener = (e: PythonEnvsChangedEvent) => unknown;
27+
28+
/**
29+
* A watcher wrapper that can be disabled.
30+
*
31+
* If disabled, events emitted by the wrapped watcher are discarded.
32+
*/
33+
export class DisableableEnvsWatcher implements IPythonEnvsWatcher {
34+
private enabled = true;
35+
constructor(
36+
// To wrap more than one use `PythonEnvWatchers`.
37+
private readonly wrapped: IPythonEnvsWatcher
38+
) {}
39+
40+
/**
41+
* Ensure that the watcher is enabled.
42+
*/
43+
public enable() {
44+
this.enabled = true;
45+
}
46+
47+
/**
48+
* Ensure that the watcher is disabled.
49+
*/
50+
public disable() {
51+
this.enabled = false;
52+
}
53+
54+
// This matches the signature of `vscode.Event`.
55+
public onChanged(listener: EnvsEventListener, thisArgs?: unknown, disposables?: Disposable[]): Disposable {
56+
return this.wrapped.onChanged(
57+
(e: PythonEnvsChangedEvent) => {
58+
if (this.enabled) {
59+
listener(e);
60+
}
61+
},
62+
thisArgs,
63+
disposables
64+
);
65+
}
66+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as assert from 'assert';
5+
import { Uri } from 'vscode';
6+
import { PythonEnvKind } from '../../../client/pythonEnvironments/base/info';
7+
import {
8+
BasicPythonEnvsChangedEvent,
9+
BasicPythonEnvsWatcher,
10+
PythonEnvsChangedEvent,
11+
PythonEnvsWatcher
12+
} from '../../../client/pythonEnvironments/base/watcher';
13+
14+
const KINDS_TO_TEST = [
15+
PythonEnvKind.Unknown,
16+
PythonEnvKind.System,
17+
PythonEnvKind.Custom,
18+
PythonEnvKind.OtherGlobal,
19+
PythonEnvKind.Venv,
20+
PythonEnvKind.Conda,
21+
PythonEnvKind.OtherVirtual
22+
];
23+
24+
suite('pyenvs watcher - BasicPythonEnvsWatcher', () => {
25+
suite('fire()', () => {
26+
test('empty event', () => {
27+
const expected: BasicPythonEnvsChangedEvent = {};
28+
const watcher = new BasicPythonEnvsWatcher();
29+
let event: BasicPythonEnvsChangedEvent | undefined;
30+
watcher.onChanged((e) => {
31+
event = e;
32+
});
33+
34+
watcher.fire(expected);
35+
36+
assert.equal(event, expected);
37+
});
38+
39+
KINDS_TO_TEST.forEach((kind) => {
40+
test(`non-empty event ("${kind}")`, () => {
41+
const expected: BasicPythonEnvsChangedEvent = {
42+
kind: kind
43+
};
44+
const watcher = new BasicPythonEnvsWatcher();
45+
let event: BasicPythonEnvsChangedEvent | undefined;
46+
watcher.onChanged((e) => {
47+
event = e;
48+
});
49+
50+
watcher.fire(expected);
51+
52+
assert.equal(event, expected);
53+
});
54+
});
55+
});
56+
57+
suite('trigger()', () => {
58+
test('empty event', () => {
59+
const expected: BasicPythonEnvsChangedEvent = {
60+
kind: undefined
61+
};
62+
const watcher = new BasicPythonEnvsWatcher();
63+
let event: BasicPythonEnvsChangedEvent | undefined;
64+
watcher.onChanged((e) => {
65+
event = e;
66+
});
67+
68+
watcher.trigger();
69+
70+
assert.deepEqual(event, expected);
71+
});
72+
73+
KINDS_TO_TEST.forEach((kind) => {
74+
test(`non-empty event ("${kind}")`, () => {
75+
const expected: BasicPythonEnvsChangedEvent = {
76+
kind: kind
77+
};
78+
const watcher = new BasicPythonEnvsWatcher();
79+
let event: BasicPythonEnvsChangedEvent | undefined;
80+
watcher.onChanged((e) => {
81+
event = e;
82+
});
83+
84+
watcher.trigger(kind);
85+
86+
assert.deepEqual(event, expected);
87+
});
88+
});
89+
});
90+
});
91+
92+
suite('pyenvs watcher - PythonEnvsWatcher', () => {
93+
const location = Uri.file('some-dir');
94+
95+
suite('fire()', () => {
96+
test('empty event', () => {
97+
const expected: PythonEnvsChangedEvent = {};
98+
const watcher = new PythonEnvsWatcher();
99+
let event: PythonEnvsChangedEvent | undefined;
100+
watcher.onChanged((e) => {
101+
event = e;
102+
});
103+
104+
watcher.fire(expected);
105+
106+
assert.equal(event, expected);
107+
});
108+
109+
KINDS_TO_TEST.forEach((kind) => {
110+
test(`non-empty event ("${kind}")`, () => {
111+
const expected: PythonEnvsChangedEvent = {
112+
kind: kind,
113+
searchLocation: location
114+
};
115+
const watcher = new PythonEnvsWatcher();
116+
let event: PythonEnvsChangedEvent | undefined;
117+
watcher.onChanged((e) => {
118+
event = e;
119+
});
120+
121+
watcher.fire(expected);
122+
123+
assert.equal(event, expected);
124+
});
125+
});
126+
});
127+
128+
suite('trigger()', () => {
129+
test('empty event', () => {
130+
const expected: PythonEnvsChangedEvent = {
131+
kind: undefined,
132+
searchLocation: undefined
133+
};
134+
const watcher = new PythonEnvsWatcher();
135+
let event: PythonEnvsChangedEvent | undefined;
136+
watcher.onChanged((e) => {
137+
event = e;
138+
});
139+
140+
watcher.trigger();
141+
142+
assert.deepEqual(event, expected);
143+
});
144+
145+
KINDS_TO_TEST.forEach((kind) => {
146+
test(`non-empty event ("${kind}")`, () => {
147+
const expected: PythonEnvsChangedEvent = {
148+
kind: kind,
149+
searchLocation: location
150+
};
151+
const watcher = new PythonEnvsWatcher();
152+
let event: PythonEnvsChangedEvent | undefined;
153+
watcher.onChanged((e) => {
154+
event = e;
155+
});
156+
157+
watcher.trigger(kind, location);
158+
159+
assert.deepEqual(event, expected);
160+
});
161+
});
162+
});
163+
});

0 commit comments

Comments
 (0)