Skip to content

Commit 0a3f220

Browse files
IanMatthewHuffrchiodo
authored andcommitted
Check our saved jupyter interpreters before allowing any of them to be used as active interpreters (#10113)
1 parent 223ce4c commit 0a3f220

File tree

6 files changed

+136
-54
lines changed

6 files changed

+136
-54
lines changed

src/client/common/process/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ export interface IPythonExecutionFactory {
113113
create(options: ExecutionFactoryCreationOptions): Promise<IPythonExecutionService>;
114114
/**
115115
* Creates a daemon Python Process.
116-
* On windows its cheapter to create a daemon and use that than spin up Python Processes everytime.
117-
* If something cannot be executed within the daemin, it will resort to using the stanard IPythonExecutionService.
116+
* On windows it's cheaper to create a daemon and use that than spin up Python Processes everytime.
117+
* If something cannot be executed within the daemon, it will resort to using the standard IPythonExecutionService.
118118
* Note: The returned execution service is always using an activated environment.
119119
*
120120
* @param {ExecutionFactoryCreationOptions} options

src/client/datascience/activation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export class Activation implements IExtensionSingleActivationService {
2828
public async activate(): Promise<void> {
2929
this.disposables.push(this.notebookProvider.onDidOpenNotebookEditor(this.onDidOpenNotebookEditor, this));
3030
this.disposables.push(this.jupyterInterpreterService.onDidChangeInterpreter(this.onDidChangeInterpreter, this));
31+
// Warm up our selected interpreter for the extension
32+
this.jupyterInterpreterService.setInitialInterpreter().ignoreErrors();
3133
await this.contextService.activate();
3234
}
3335

src/client/datascience/jupyter/interpreter/jupyterInterpreterService.ts

Lines changed: 99 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Event, EventEmitter } from 'vscode';
88
import { CancellationToken } from 'vscode-jsonrpc';
99
import { createPromiseFromCancellation } from '../../../common/cancellation';
1010
import '../../../common/extensions';
11+
import { noop } from '../../../common/utils/misc';
1112
import { IInterpreterService, PythonInterpreter } from '../../../interpreter/contracts';
1213
import { sendTelemetryEvent } from '../../../telemetry';
1314
import { Telemetry } from '../../constants';
@@ -19,8 +20,8 @@ import { JupyterInterpreterStateStore } from './jupyterInterpreterStateStore';
1920
@injectable()
2021
export class JupyterInterpreterService {
2122
private _selectedInterpreter?: PythonInterpreter;
22-
private _selectedInterpreterPath?: string;
2323
private _onDidChangeInterpreter = new EventEmitter<PythonInterpreter>();
24+
private getInitialInterpreterPromise: Promise<PythonInterpreter | undefined> | undefined;
2425
public get onDidChangeInterpreter(): Event<PythonInterpreter> {
2526
return this._onDidChangeInterpreter.event;
2627
}
@@ -40,40 +41,30 @@ export class JupyterInterpreterService {
4041
* @memberof JupyterInterpreterService
4142
*/
4243
public async getSelectedInterpreter(token?: CancellationToken): Promise<PythonInterpreter | undefined> {
43-
if (this._selectedInterpreter) {
44-
return this._selectedInterpreter;
45-
}
44+
// Before we return _selected interpreter make sure that we have run our initial set interpreter once
45+
// because _selectedInterpreter can be changed by other function and at other times, this promise
46+
// is cached to only run once
47+
await this.setInitialInterpreter(token);
4648

47-
const resolveToUndefinedWhenCancelled = createPromiseFromCancellation({ cancelAction: 'resolve', defaultValue: undefined, token });
48-
// For backwards compatiblity check if we have a cached interpreter (older version of extension).
49-
// If that interpreter has everything we need then use that.
50-
let interpreter = await Promise.race([this.getInterpreterFromChangeOfOlderVersionOfExtension(), resolveToUndefinedWhenCancelled]);
51-
if (interpreter) {
52-
return interpreter;
53-
}
49+
return this._selectedInterpreter;
50+
}
5451

55-
const pythonPath = this._selectedInterpreterPath || this.interpreterSelectionState.selectedPythonPath;
56-
if (!pythonPath) {
57-
// Check if current interpreter has all of the required dependencies.
58-
// If yes, then use that.
59-
interpreter = await this.interpreterService.getActiveInterpreter(undefined);
60-
if (!interpreter) {
61-
return;
62-
}
63-
// Use this interpreter going forward.
64-
if (await this.interpreterConfiguration.areDependenciesInstalled(interpreter)) {
65-
this.setAsSelectedInterpreter(interpreter);
66-
return interpreter;
67-
}
68-
return;
52+
// To be run one initial time. Check our saved locations and then current interpreter to try to start off
53+
// with a valid jupyter interpreter
54+
public async setInitialInterpreter(token?: CancellationToken): Promise<PythonInterpreter | undefined> {
55+
if (!this.getInitialInterpreterPromise) {
56+
this.getInitialInterpreterPromise = this.getInitialInterpreterImpl(token).then(result => {
57+
// Set ourselves as a valid interpreter if we found something
58+
if (result) {
59+
this.changeSelectedInterpreterProperty(result);
60+
}
61+
return result;
62+
});
6963
}
7064

71-
const interpreterDetails = await Promise.race([this.interpreterService.getInterpreterDetails(pythonPath, undefined), resolveToUndefinedWhenCancelled]);
72-
if (interpreterDetails) {
73-
this._selectedInterpreter = interpreterDetails;
74-
}
75-
return interpreterDetails;
65+
return this.getInitialInterpreterPromise;
7666
}
67+
7768
/**
7869
* Selects and interpreter to run jupyter server.
7970
* Validates and configures the interpreter.
@@ -94,7 +85,7 @@ export class JupyterInterpreterService {
9485
const result = await this.interpreterConfiguration.installMissingDependencies(interpreter, undefined, token);
9586
switch (result) {
9687
case JupyterInterpreterDependencyResponse.ok: {
97-
this.setAsSelectedInterpreter(interpreter);
88+
await this.setAsSelectedInterpreter(interpreter);
9889
return interpreter;
9990
}
10091
case JupyterInterpreterDependencyResponse.cancel:
@@ -104,30 +95,91 @@ export class JupyterInterpreterService {
10495
return this.selectInterpreter(token);
10596
}
10697
}
107-
private async getInterpreterFromChangeOfOlderVersionOfExtension(): Promise<PythonInterpreter | undefined> {
98+
99+
// Check the location that we stored jupyter launch path in the old version
100+
// if it's there, return it and clear the location
101+
private getInterpreterFromChangeOfOlderVersionOfExtension(): string | undefined {
108102
const pythonPath = this.oldVersionCacheStateStore.getCachedInterpreterPath();
109103
if (!pythonPath) {
110104
return;
111105
}
106+
107+
// Clear the cache to not check again
108+
this.oldVersionCacheStateStore.clearCache().ignoreErrors();
109+
return pythonPath;
110+
}
111+
112+
// Set the specified interpreter as our current selected interpreter
113+
private async setAsSelectedInterpreter(interpreter: PythonInterpreter): Promise<void> {
114+
// Make sure that our initial set has happened before we allow a set so that
115+
// calculation of the initial interpreter doesn't clobber the existing one
116+
await this.setInitialInterpreter();
117+
this.changeSelectedInterpreterProperty(interpreter);
118+
}
119+
120+
private changeSelectedInterpreterProperty(interpreter: PythonInterpreter) {
121+
this._selectedInterpreter = interpreter;
122+
this._onDidChangeInterpreter.fire(interpreter);
123+
this.interpreterSelectionState.updateSelectedPythonPath(interpreter.path);
124+
sendTelemetryEvent(Telemetry.SelectJupyterInterpreter, undefined, { result: 'selected' });
125+
}
126+
127+
// For a given python path check if it can run jupyter for us
128+
// if so, return the interpreter
129+
private async validateInterpreterPath(pythonPath: string, token?: CancellationToken): Promise<PythonInterpreter | undefined> {
112130
try {
113-
const interpreter = await this.interpreterService.getInterpreterDetails(pythonPath, undefined);
131+
const resolveToUndefinedWhenCancelled = createPromiseFromCancellation({
132+
cancelAction: 'resolve',
133+
defaultValue: undefined,
134+
token
135+
});
136+
137+
// First see if we can get interpreter details
138+
const interpreter = await Promise.race([this.interpreterService.getInterpreterDetails(pythonPath, undefined), resolveToUndefinedWhenCancelled]);
139+
if (interpreter) {
140+
// Then check that dependencies are installed
141+
if (await this.interpreterConfiguration.areDependenciesInstalled(interpreter, token)) {
142+
return interpreter;
143+
}
144+
}
145+
} catch (_err) {
146+
// For any errors we are ok with just returning undefined for an invalid interpreter
147+
noop();
148+
}
149+
return undefined;
150+
}
151+
152+
private async getInitialInterpreterImpl(token?: CancellationToken): Promise<PythonInterpreter | undefined> {
153+
let interpreter: PythonInterpreter | undefined;
154+
155+
// Check the old version location first, we will clear it if we find it here
156+
const oldVersionPythonPath = this.getInterpreterFromChangeOfOlderVersionOfExtension();
157+
if (oldVersionPythonPath) {
158+
interpreter = await this.validateInterpreterPath(oldVersionPythonPath, token);
159+
}
160+
161+
// Next check the saved global path
162+
if (!interpreter && this.interpreterSelectionState.selectedPythonPath) {
163+
interpreter = await this.validateInterpreterPath(this.interpreterSelectionState.selectedPythonPath, token);
164+
165+
// If we had a global path, but it's not valid, trash it
114166
if (!interpreter) {
115-
return;
167+
this.interpreterSelectionState.updateSelectedPythonPath(undefined);
116168
}
117-
if (await this.interpreterConfiguration.areDependenciesInstalled(interpreter)) {
118-
this.setAsSelectedInterpreter(interpreter);
119-
return interpreter;
169+
}
170+
171+
// Nothing saved found, so check our current interpreter
172+
if (!interpreter) {
173+
const currentInterpreter = await this.interpreterService.getActiveInterpreter(undefined);
174+
175+
if (currentInterpreter) {
176+
// Ask and give a chance to install dependencies in current interpreter
177+
if (await this.interpreterConfiguration.areDependenciesInstalled(currentInterpreter, token)) {
178+
interpreter = currentInterpreter;
179+
}
120180
}
121-
// If dependencies are not installed, then ignore it. lets continue with the current logic.
122-
} finally {
123-
// Don't perform this check again, just clear the cache.
124-
this.oldVersionCacheStateStore.clearCache().ignoreErrors();
125181
}
126-
}
127-
private setAsSelectedInterpreter(interpreter: PythonInterpreter): void {
128-
this._selectedInterpreter = interpreter;
129-
this._onDidChangeInterpreter.fire(interpreter);
130-
this.interpreterSelectionState.updateSelectedPythonPath((this._selectedInterpreterPath = interpreter.path));
131-
sendTelemetryEvent(Telemetry.SelectJupyterInterpreter, undefined, { result: 'selected' });
182+
183+
return interpreter;
132184
}
133185
}

src/client/datascience/jupyter/interpreter/jupyterInterpreterStateStore.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import { noop } from '../../../common/utils/misc';
1111
const key = 'INTERPRETER_PATH_SELECTED_FOR_JUPYTER_SERVER';
1212
const keySelected = 'INTERPRETER_PATH_WAS_SELECTED_FOR_JUPYTER_SERVER';
1313
/**
14-
* Keeps track of whether the user ever selected an interpreter to be used as the gloabl jupyter interpreter.
14+
* Keeps track of whether the user ever selected an interpreter to be used as the global jupyter interpreter.
1515
* Keeps track of the interpreter path of the interpreter used as the global jupyter interpreter.
1616
*
1717
* @export
18-
* @class JupyterInterpreterFinderEverSet
18+
* @class JupyterInterpreterStateStore
1919
*/
2020
@injectable()
2121
export class JupyterInterpreterStateStore {
@@ -27,15 +27,14 @@ export class JupyterInterpreterStateStore {
2727
*
2828
* @readonly
2929
* @type {Promise<boolean>}
30-
* @memberof JupyterInterpreterFinderEverSet
3130
*/
3231
public get interpreterSetAtleastOnce(): boolean {
3332
return !!this.selectedPythonPath || this.memento.get<boolean>(keySelected, false);
3433
}
3534
public get selectedPythonPath(): string | undefined {
3635
return this._interpreterPath || this.memento.get<string | undefined>(key, undefined);
3736
}
38-
public updateSelectedPythonPath(value: string) {
37+
public updateSelectedPythonPath(value: string | undefined) {
3938
this._interpreterPath = value;
4039
this.memento.update(key, value).then(noop, noop);
4140
this.memento.update(keySelected, true).then(noop, noop);

src/test/datascience/activation.unit.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ suite('Data Science - Activation', () => {
4747
activator = new Activation(instance(notebookProvider), instance(jupyterInterpreterService), instance(executionFactory), [], instance(contextService));
4848
when(jupyterInterpreterService.getSelectedInterpreter()).thenResolve(interpreter);
4949
when(jupyterInterpreterService.getSelectedInterpreter(anything())).thenResolve(interpreter);
50+
when(jupyterInterpreterService.setInitialInterpreter()).thenResolve(interpreter);
5051
await activator.activate();
5152
});
5253
teardown(() => fakeTimer.uninstall());

src/test/datascience/jupyter/interpreter/jupyterInterpreterService.unit.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
'use strict';
55

66
import { assert } from 'chai';
7-
import { anything, instance, mock, verify, when } from 'ts-mockito';
7+
import { anyString, anything, instance, mock, verify, when } from 'ts-mockito';
88
import { Memento } from 'vscode';
99
import { Architecture } from '../../../../client/common/utils/platform';
1010
import { JupyterInterpreterDependencyResponse, JupyterInterpreterDependencyService } from '../../../../client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService';
@@ -107,4 +107,32 @@ suite('Data Science - Jupyter Interpreter Service', () => {
107107

108108
assert.equal(selectedInterpreter, secondPythonInterpreter);
109109
});
110+
test('setInitialInterpreter if older version is set should use and clear', async () => {
111+
when(oldVersionCacheStateStore.getCachedInterpreterPath()).thenReturn(pythonInterpreter.path);
112+
when(oldVersionCacheStateStore.clearCache()).thenResolve();
113+
when(interpreterConfiguration.areDependenciesInstalled(pythonInterpreter, anything())).thenResolve(true);
114+
const initialInterpreter = await jupyterInterpreterService.setInitialInterpreter(undefined);
115+
verify(oldVersionCacheStateStore.clearCache()).once();
116+
assert.equal(initialInterpreter, pythonInterpreter);
117+
});
118+
test('setInitialInterpreter use saved interpreter if valid', async () => {
119+
when(oldVersionCacheStateStore.getCachedInterpreterPath()).thenReturn(undefined);
120+
when(interpreterSelectionState.selectedPythonPath).thenReturn(pythonInterpreter.path);
121+
when(interpreterConfiguration.areDependenciesInstalled(pythonInterpreter, anything())).thenResolve(true);
122+
const initialInterpreter = await jupyterInterpreterService.setInitialInterpreter(undefined);
123+
assert.equal(initialInterpreter, pythonInterpreter);
124+
});
125+
test('setInitialInterpreter saved interpreter invalid, clear it and use active interpreter', async () => {
126+
when(oldVersionCacheStateStore.getCachedInterpreterPath()).thenReturn(undefined);
127+
when(interpreterSelectionState.selectedPythonPath).thenReturn(secondPythonInterpreter.path);
128+
when(interpreterConfiguration.areDependenciesInstalled(secondPythonInterpreter, anything())).thenResolve(false);
129+
when(interpreterService.getActiveInterpreter(anything())).thenResolve(pythonInterpreter);
130+
when(interpreterConfiguration.areDependenciesInstalled(pythonInterpreter, anything())).thenResolve(true);
131+
const initialInterpreter = await jupyterInterpreterService.setInitialInterpreter(undefined);
132+
assert.equal(initialInterpreter, pythonInterpreter);
133+
// Make sure we set our saved interpreter to the new active interpreter
134+
// it should have been cleared to undefined, then set to a new value
135+
verify(interpreterSelectionState.updateSelectedPythonPath(undefined)).once();
136+
verify(interpreterSelectionState.updateSelectedPythonPath(anyString())).once();
137+
});
110138
});

0 commit comments

Comments
 (0)