Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions src/client/pythonEnvironments/base/watcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

// tslint:disable:max-classes-per-file

import { Event, EventEmitter, Uri } from 'vscode';
import { PythonEnvKind } from './info';

/**
* The most basic info for a Python environments event.
*
* @prop kind - the env kind, if any, affected by the event
*/
export type BasicPythonEnvsChangedEvent = {
kind?: PythonEnvKind;
};

/**
* The full set of possible info for a Python environments event.
*
* @prop searchLocation - the location, if any, affected by the event
*/
export type PythonEnvsChangedEvent = BasicPythonEnvsChangedEvent & {
searchLocation?: Uri;
};

/**
* A "watcher" for events related to changes to Python environemts.
*
* The watcher will notify listeners (callbacks registered through
* `onChanged`) of events at undetermined times. The actual emitted
* events, their source, and the timing is entirely up to the watcher
* implementation.
*/
export interface IPythonEnvsWatcher<E extends BasicPythonEnvsChangedEvent = PythonEnvsChangedEvent> {
/**
* The hook for registering event listeners (callbacks).
*/
readonly onChanged: Event<E>;
}

/**
* This provides the fundamental functionality of a watcher for any event type.
*
* Consumers register listeners (callbacks) using `onChanged`. Each
* listener is invoked when `fire()` is called.
*
* Note that in most cases classes will not inherit from this classes,
* but instead keep a private watcher property. The rule of thumb
* is to follow whether or not consumers of *that* class should be able
* to trigger events (via `fire()`).
*/
class WatcherBase<T> implements IPythonEnvsWatcher<T> {
/**
* The hook for registering event listeners (callbacks).
*/
public readonly onChanged: Event<T>;
private readonly didChange = new EventEmitter<T>();

constructor() {
this.onChanged = this.didChange.event;
}

/**
* Send the event to all registered listeners.
*/
public fire(event: T) {
this.didChange.fire(event);
}
}

// The use cases for BasicPythonEnvsWatcher are currently hypothetical.
// However, there's a real chance they may prove useful for the concrete
// locators. Adding BasicPythonEnvsWatcher later will be much harder
// than removing it later, so we're leaving it for now.

/**
* A watcher for the basic Python environments events.
*
* This should be used only in low-level cases, with the most
* rudimentary watchers. Most of the time `PythonEnvsWatcher`
* should be used instead.
*
* Note that in most cases classes will not inherit from this classes,
* but instead keep a private watcher property. The rule of thumb
* is to follow whether or not consumers of *that* class should be able
* to trigger events (via `fire()`).
*/
export class BasicPythonEnvsWatcher extends WatcherBase<BasicPythonEnvsChangedEvent> {
Comment thread
ericsnowcurrently marked this conversation as resolved.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In which use cases would we use a BasicPythonEnvsWatcher instead of a PythonEnvsWatcher? i.e why are we exporting it, is it just for tests?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's for the case that a watcher does not possibly provide non-basic info (currently only searchLocation). That said, it is somewhat of an artifact of an earlier design I was exploring, where the actual locators (not wrappers) were all basic and wrappers provided the non-basic info.

There isn't much cost to leaving BasicPythonEnvsWatcher for now, adding it later would be more painful than removing it later, and my gut is telling me that it will be useful later, so I'd like to leave it. We can circle back on removing it after we have refactored our existing locators. Is there any additional clarification I could put in a comment that would help avoid any confusion about why BasicPythonEnvsWatcher exists?

/**
* Fire an event based on the given info.
*/
public trigger(kind?: PythonEnvKind) {
this.fire({ kind });
}
}

/**
* A general-use watcher for Python environments events.
*
* In most cases this is the class you will want to use or subclass.
* Only in low-level cases should you consider using `BasicPythonEnvsWatcher`.
*
* Note that in most cases classes will not inherit from this classes,
* but instead keep a private watcher property. The rule of thumb
* is to follow whether or not consumers of *that* class should be able
* to trigger events (via `fire()`).
*/
export class PythonEnvsWatcher extends WatcherBase<PythonEnvsChangedEvent> {
/**
* Fire an event based on the given info.
*/
public trigger(kind?: PythonEnvKind, searchLocation?: Uri) {
this.fire({ kind, searchLocation });
}
}
66 changes: 66 additions & 0 deletions src/client/pythonEnvironments/base/watchers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { Disposable, Event } from 'vscode';
import { IPythonEnvsWatcher, PythonEnvsChangedEvent, PythonEnvsWatcher } from './watcher';

/**
* A wrapper around a set of watchers, exposing them as a single watcher.
*
* If any of the wrapped watchers emits an event then this wrapper
* emits that event.
*/
export class PythonEnvsWatchers implements IPythonEnvsWatcher {
public readonly onChanged: Event<PythonEnvsChangedEvent>;
private watcher = new PythonEnvsWatcher();

constructor(watchers: ReadonlyArray<IPythonEnvsWatcher>) {
this.onChanged = this.watcher.onChanged;
watchers.forEach((w) => {
w.onChanged((e) => this.watcher.fire(e));
});
}
}

// This matches the `vscode.Event` arg.
type EnvsEventListener = (e: PythonEnvsChangedEvent) => unknown;

/**
* A watcher wrapper that can be disabled.
*
* If disabled, events emitted by the wrapped watcher are discarded.
*/
export class DisableableEnvsWatcher implements IPythonEnvsWatcher {
private enabled = true;
constructor(
// To wrap more than one use `PythonEnvWatchers`.
private readonly wrapped: IPythonEnvsWatcher
Comment thread
ericsnowcurrently marked this conversation as resolved.
) {}

/**
* Ensure that the watcher is enabled.
*/
public enable() {
this.enabled = true;
}

/**
* Ensure that the watcher is disabled.
*/
public disable() {
this.enabled = false;
}

// This matches the signature of `vscode.Event`.
public onChanged(listener: EnvsEventListener, thisArgs?: unknown, disposables?: Disposable[]): Disposable {
return this.wrapped.onChanged(
(e: PythonEnvsChangedEvent) => {
if (this.enabled) {
listener(e);
}
},
thisArgs,
disposables
);
}
}
163 changes: 163 additions & 0 deletions src/test/pythonEnvironments/base/watcher.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import * as assert from 'assert';
import { Uri } from 'vscode';
import { PythonEnvKind } from '../../../client/pythonEnvironments/base/info';
import {
BasicPythonEnvsChangedEvent,
BasicPythonEnvsWatcher,
PythonEnvsChangedEvent,
PythonEnvsWatcher
} from '../../../client/pythonEnvironments/base/watcher';

const KINDS_TO_TEST = [
PythonEnvKind.Unknown,
PythonEnvKind.System,
PythonEnvKind.Custom,
PythonEnvKind.OtherGlobal,
PythonEnvKind.Venv,
PythonEnvKind.Conda,
PythonEnvKind.OtherVirtual
];

suite('pyenvs watcher - BasicPythonEnvsWatcher', () => {
suite('fire()', () => {
test('empty event', () => {
const expected: BasicPythonEnvsChangedEvent = {};
const watcher = new BasicPythonEnvsWatcher();
let event: BasicPythonEnvsChangedEvent | undefined;
watcher.onChanged((e) => {
event = e;
});

watcher.fire(expected);

assert.equal(event, expected);
});

KINDS_TO_TEST.forEach((kind) => {
test(`non-empty event ("${kind}")`, () => {
const expected: BasicPythonEnvsChangedEvent = {
kind: kind
};
const watcher = new BasicPythonEnvsWatcher();
let event: BasicPythonEnvsChangedEvent | undefined;
watcher.onChanged((e) => {
event = e;
});

watcher.fire(expected);

assert.equal(event, expected);
});
});
});

suite('trigger()', () => {
test('empty event', () => {
const expected: BasicPythonEnvsChangedEvent = {
kind: undefined
};
const watcher = new BasicPythonEnvsWatcher();
let event: BasicPythonEnvsChangedEvent | undefined;
watcher.onChanged((e) => {
event = e;
});

watcher.trigger();

assert.deepEqual(event, expected);
});

KINDS_TO_TEST.forEach((kind) => {
test(`non-empty event ("${kind}")`, () => {
const expected: BasicPythonEnvsChangedEvent = {
kind: kind
};
const watcher = new BasicPythonEnvsWatcher();
let event: BasicPythonEnvsChangedEvent | undefined;
watcher.onChanged((e) => {
event = e;
});

watcher.trigger(kind);

assert.deepEqual(event, expected);
});
});
});
});

suite('pyenvs watcher - PythonEnvsWatcher', () => {
const location = Uri.file('some-dir');

suite('fire()', () => {
test('empty event', () => {
const expected: PythonEnvsChangedEvent = {};
const watcher = new PythonEnvsWatcher();
let event: PythonEnvsChangedEvent | undefined;
watcher.onChanged((e) => {
event = e;
});

watcher.fire(expected);

assert.equal(event, expected);
});

KINDS_TO_TEST.forEach((kind) => {
test(`non-empty event ("${kind}")`, () => {
const expected: PythonEnvsChangedEvent = {
kind: kind,
searchLocation: location
};
const watcher = new PythonEnvsWatcher();
let event: PythonEnvsChangedEvent | undefined;
watcher.onChanged((e) => {
event = e;
});

watcher.fire(expected);

assert.equal(event, expected);
});
});
});

suite('trigger()', () => {
test('empty event', () => {
const expected: PythonEnvsChangedEvent = {
kind: undefined,
searchLocation: undefined
};
const watcher = new PythonEnvsWatcher();
let event: PythonEnvsChangedEvent | undefined;
watcher.onChanged((e) => {
event = e;
});

watcher.trigger();

assert.deepEqual(event, expected);
});

KINDS_TO_TEST.forEach((kind) => {
test(`non-empty event ("${kind}")`, () => {
const expected: PythonEnvsChangedEvent = {
kind: kind,
searchLocation: location
};
const watcher = new PythonEnvsWatcher();
let event: PythonEnvsChangedEvent | undefined;
watcher.onChanged((e) => {
event = e;
});

watcher.trigger(kind, location);

assert.deepEqual(event, expected);
});
});
});
});
Loading