Skip to content

Commit 0b818f8

Browse files
committed
Fix to return env variables of interpreter that is not current interpreter (#10251)
For #10250 * Bug fix
1 parent 4dc2d43 commit 0b818f8

File tree

6 files changed

+140
-30
lines changed

6 files changed

+140
-30
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Changelog
22

3+
## 2020.2.2 (20 February 2020)
4+
35
## 2020.2.2 (19 February 2020)
46

57
### Fixes

news/2 Fixes/10250.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Ensure to correctly return env variables of the activated interpreter, when dealing with non-workspace interpreters.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "python",
33
"displayName": "Python",
44
"description": "Linting, Debugging (multi-threaded, remote), Intellisense, Jupyter Notebooks, code formatting, refactoring, unit tests, snippets, and more.",
5-
"version": "2020.2.2",
5+
"version": "2020.2.3",
66
"languageServerVersion": "0.5.30",
77
"publisher": "ms-python",
88
"author": {

src/client/common/logger.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,11 @@ function trace(message: string, options: LogOptions = LogOptions.None, logLevel?
285285
// tslint:disable-next-line:no-any
286286
function writeToLog(elapsedTime: number, returnValue?: any, ex?: Error) {
287287
const messagesToLog = [message];
288-
messagesToLog.push(`Class name = ${className}, completed in ${elapsedTime}ms`);
288+
messagesToLog.push(
289+
`Class name = ${className}, completed in ${elapsedTime}ms, has a ${
290+
returnValue ? 'truthy' : 'falsy'
291+
} return value`
292+
);
289293
if ((options && LogOptions.Arguments) === LogOptions.Arguments) {
290294
messagesToLog.push(argsToLogString(args));
291295
}

src/client/interpreter/activation/service.ts

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,24 @@ import '../../common/extensions';
66
import { inject, injectable } from 'inversify';
77
import * as path from 'path';
88

9+
import { IWorkspaceService } from '../../common/application/types';
910
import { PYTHON_WARNINGS } from '../../common/constants';
1011
import { LogOptions, traceDecorators, traceError, traceVerbose } from '../../common/logger';
1112
import { IPlatformService } from '../../common/platform/types';
1213
import { IProcessServiceFactory } from '../../common/process/types';
1314
import { ITerminalHelper, TerminalShellType } from '../../common/terminal/types';
1415
import { ICurrentProcess, IDisposable, Resource } from '../../common/types';
16+
<<<<<<< HEAD
1517
import { cacheResourceSpecificInterpreterData, clearCachedResourceSpecificIngterpreterData } from '../../common/utils/decorators';
18+
=======
19+
import { InMemoryCache } from '../../common/utils/cacheUtils';
20+
>>>>>>> ae86ae46f... Fix to return env variables of interpreter that is not current interpreter (#10251)
1621
import { OSType } from '../../common/utils/platform';
1722
import { IEnvironmentVariablesProvider } from '../../common/variables/types';
1823
import { EXTENSION_ROOT_DIR } from '../../constants';
1924
import { captureTelemetry, sendTelemetryEvent } from '../../telemetry';
2025
import { EventName } from '../../telemetry/constants';
21-
import { PythonInterpreter } from '../contracts';
26+
import { IInterpreterService, PythonInterpreter } from '../contracts';
2227
import { IEnvironmentActivationService } from './types';
2328

2429
const getEnvironmentPrefix = 'e8b39361-0157-4923-80e1-22d70d46dee6';
@@ -36,23 +41,62 @@ const defaultShells = {
3641
@injectable()
3742
export class EnvironmentActivationService implements IEnvironmentActivationService, IDisposable {
3843
private readonly disposables: IDisposable[] = [];
44+
private readonly activatedEnvVariablesCache = new Map<string, InMemoryCache<NodeJS.ProcessEnv | undefined>>();
3945
constructor(
4046
@inject(ITerminalHelper) private readonly helper: ITerminalHelper,
4147
@inject(IPlatformService) private readonly platform: IPlatformService,
4248
@inject(IProcessServiceFactory) private processServiceFactory: IProcessServiceFactory,
4349
@inject(ICurrentProcess) private currentProcess: ICurrentProcess,
50+
@inject(IWorkspaceService) private workspace: IWorkspaceService,
51+
@inject(IInterpreterService) private interpreterService: IInterpreterService,
4452
@inject(IEnvironmentVariablesProvider) private readonly envVarsService: IEnvironmentVariablesProvider
4553
) {
46-
this.envVarsService.onDidEnvironmentVariablesChange(this.onDidEnvironmentVariablesChange, this, this.disposables);
54+
this.envVarsService.onDidEnvironmentVariablesChange(
55+
() => this.activatedEnvVariablesCache.clear(),
56+
this,
57+
this.disposables
58+
);
59+
60+
this.interpreterService.onDidChangeInterpreter(
61+
() => this.activatedEnvVariablesCache.clear(),
62+
this,
63+
this.disposables
64+
);
4765
}
4866

4967
public dispose(): void {
5068
this.disposables.forEach(d => d.dispose());
5169
}
5270
@traceDecorators.verbose('getActivatedEnvironmentVariables', LogOptions.Arguments)
5371
@captureTelemetry(EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES, { failed: false }, true)
54-
@cacheResourceSpecificInterpreterData('ActivatedEnvironmentVariables', cacheDuration)
55-
public async getActivatedEnvironmentVariables(resource: Resource, interpreter?: PythonInterpreter, allowExceptions?: boolean): Promise<NodeJS.ProcessEnv | undefined> {
72+
public async getActivatedEnvironmentVariables(
73+
resource: Resource,
74+
interpreter?: PythonInterpreter,
75+
allowExceptions?: boolean
76+
): Promise<NodeJS.ProcessEnv | undefined> {
77+
// Cache key = resource + interpreter.
78+
const workspaceKey = this.workspace.getWorkspaceFolderIdentifier(resource);
79+
const interpreterPath = this.platform.isWindows ? interpreter?.path.toLowerCase() : interpreter?.path;
80+
const cacheKey = `${workspaceKey}_${interpreterPath}`;
81+
82+
if (this.activatedEnvVariablesCache.get(cacheKey)?.hasData) {
83+
return this.activatedEnvVariablesCache.get(cacheKey)!.data;
84+
}
85+
86+
// Cache only if successful, else keep trying & failing if necessary.
87+
const cache = new InMemoryCache<NodeJS.ProcessEnv | undefined>(cacheDuration, '');
88+
return this.getActivatedEnvironmentVariablesImpl(resource, interpreter, allowExceptions).then(vars => {
89+
cache.data = vars;
90+
this.activatedEnvVariablesCache.set(cacheKey, cache);
91+
return vars;
92+
});
93+
}
94+
95+
public async getActivatedEnvironmentVariablesImpl(
96+
resource: Resource,
97+
interpreter?: PythonInterpreter,
98+
allowExceptions?: boolean
99+
): Promise<NodeJS.ProcessEnv | undefined> {
56100
const shellInfo = defaultShells[this.platform.osType];
57101
if (!shellInfo) {
58102
return;
@@ -115,9 +159,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi
115159
}
116160
}
117161
}
118-
protected onDidEnvironmentVariablesChange(affectedResource: Resource) {
119-
clearCachedResourceSpecificIngterpreterData('ActivatedEnvironmentVariables', affectedResource);
120-
}
162+
121163
protected fixActivationCommands(commands: string[]): string[] {
122164
// Replace 'source ' with '. ' as that works in shell exec
123165
return commands.map(cmd => cmd.replace(/^source\s+/, '. '));

src/test/interpreters/activation/service.unit.test.ts

Lines changed: 82 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { EOL } from 'os';
77
import * as path from 'path';
88
import { SemVer } from 'semver';
99
import { anything, capture, instance, mock, verify, when } from 'ts-mockito';
10-
import * as typemoq from 'typemoq';
11-
import { EventEmitter, Uri, workspace as workspaceType, WorkspaceConfiguration } from 'vscode';
10+
import { EventEmitter, Uri } from 'vscode';
11+
import { IWorkspaceService } from '../../../client/common/application/types';
12+
import { WorkspaceService } from '../../../client/common/application/workspace';
1213
import { PlatformService } from '../../../client/common/platform/platformService';
1314
import { IPlatformService } from '../../../client/common/platform/types';
1415
import { CurrentProcess } from '../../../client/common/process/currentProcess';
@@ -18,15 +19,14 @@ import { IProcessService, IProcessServiceFactory } from '../../../client/common/
1819
import { TerminalHelper } from '../../../client/common/terminal/helper';
1920
import { ITerminalHelper } from '../../../client/common/terminal/types';
2021
import { ICurrentProcess } from '../../../client/common/types';
21-
import { clearCache } from '../../../client/common/utils/cacheUtils';
2222
import { getNamesAndValues } from '../../../client/common/utils/enum';
2323
import { Architecture, OSType } from '../../../client/common/utils/platform';
2424
import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider';
2525
import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types';
2626
import { EXTENSION_ROOT_DIR } from '../../../client/constants';
2727
import { EnvironmentActivationService } from '../../../client/interpreter/activation/service';
28-
import { InterpreterType, PythonInterpreter } from '../../../client/interpreter/contracts';
29-
import { mockedVSCodeNamespaces } from '../../vscode-mock';
28+
import { IInterpreterService, InterpreterType, PythonInterpreter } from '../../../client/interpreter/contracts';
29+
import { InterpreterService } from '../../../client/interpreter/interpreterService';
3030

3131
const getEnvironmentPrefix = 'e8b39361-0157-4923-80e1-22d70d46dee6';
3232
const defaultShells = {
@@ -45,8 +45,10 @@ suite('Interpreters Activation - Python Environment Variables', () => {
4545
let processService: IProcessService;
4646
let currentProcess: ICurrentProcess;
4747
let envVarsService: IEnvironmentVariablesProvider;
48-
let workspace: typemoq.IMock<typeof workspaceType>;
49-
48+
let workspace: IWorkspaceService;
49+
let interpreterService: IInterpreterService;
50+
let onDidChangeEnvVariables: EventEmitter<Uri | undefined>;
51+
let onDidChangeInterpreter: EventEmitter<void>;
5052
const pythonInterpreter: PythonInterpreter = {
5153
path: '/foo/bar/python.exe',
5254
version: new SemVer('3.6.6-final'),
@@ -63,21 +65,22 @@ suite('Interpreters Activation - Python Environment Variables', () => {
6365
processService = mock(ProcessService);
6466
currentProcess = mock(CurrentProcess);
6567
envVarsService = mock(EnvironmentVariablesProvider);
66-
workspace = mockedVSCodeNamespaces.workspace!;
67-
when(envVarsService.onDidEnvironmentVariablesChange).thenReturn(new EventEmitter<Uri | undefined>().event);
68-
service = new EnvironmentActivationService(instance(helper), instance(platform), instance(processServiceFactory), instance(currentProcess), instance(envVarsService));
69-
70-
const cfg = typemoq.Mock.ofType<WorkspaceConfiguration>();
71-
workspace.setup(w => w.getConfiguration(typemoq.It.isValue('python'), typemoq.It.isAny())).returns(() => cfg.object);
72-
workspace.setup(w => w.workspaceFolders).returns(() => []);
73-
cfg.setup(c => c.inspect(typemoq.It.isValue('pythonPath'))).returns(() => {
74-
return { globalValue: 'GlobalValuepython' } as any;
75-
});
76-
clearCache();
68+
interpreterService = mock(InterpreterService);
69+
workspace = mock(WorkspaceService);
70+
onDidChangeEnvVariables = new EventEmitter<Uri | undefined>();
71+
onDidChangeInterpreter = new EventEmitter<void>();
72+
when(envVarsService.onDidEnvironmentVariablesChange).thenReturn(onDidChangeEnvVariables.event);
73+
when(interpreterService.onDidChangeInterpreter).thenReturn(onDidChangeInterpreter.event);
74+
service = new EnvironmentActivationService(
75+
instance(helper),
76+
instance(platform),
77+
instance(processServiceFactory),
78+
instance(currentProcess),
79+
instance(workspace),
80+
instance(interpreterService),
81+
instance(envVarsService)
82+
);
7783
}
78-
teardown(() => {
79-
mockedVSCodeNamespaces.workspace!.reset();
80-
});
8184

8285
function title(resource?: Uri, interpreter?: PythonInterpreter) {
8386
return `${resource ? 'With a resource' : 'Without a resource'}${interpreter ? ' and an interpreter' : ''}`;
@@ -218,6 +221,64 @@ suite('Interpreters Activation - Python Environment Variables', () => {
218221
verify(envVarsService.getEnvironmentVariables(resource)).once();
219222
verify(processService.shellExec(anything(), anything())).once();
220223
});
224+
test('Cache Variables', async () => {
225+
const cmd = ['1', '2'];
226+
const varsFromEnv = { one: '11', two: '22', HELLO: 'xxx' };
227+
const stdout = `${getEnvironmentPrefix}${EOL}${JSON.stringify(varsFromEnv)}`;
228+
when(platform.osType).thenReturn(osType.value);
229+
when(helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter)).thenResolve(cmd);
230+
when(processServiceFactory.create(resource)).thenResolve(instance(processService));
231+
when(envVarsService.getEnvironmentVariables(resource)).thenResolve({});
232+
when(processService.shellExec(anything(), anything())).thenResolve({ stdout: stdout });
233+
234+
const env = await service.getActivatedEnvironmentVariables(resource, interpreter);
235+
const env2 = await service.getActivatedEnvironmentVariables(resource, interpreter);
236+
const env3 = await service.getActivatedEnvironmentVariables(resource, interpreter);
237+
238+
expect(env).to.deep.equal(varsFromEnv);
239+
// All same objects.
240+
expect(env)
241+
.to.equal(env2)
242+
.to.equal(env3);
243+
244+
// All methods invoked only once.
245+
verify(helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter)).once();
246+
verify(processServiceFactory.create(resource)).once();
247+
verify(envVarsService.getEnvironmentVariables(resource)).once();
248+
verify(processService.shellExec(anything(), anything())).once();
249+
});
250+
async function testClearingCache(bustCache: Function) {
251+
const cmd = ['1', '2'];
252+
const varsFromEnv = { one: '11', two: '22', HELLO: 'xxx' };
253+
const stdout = `${getEnvironmentPrefix}${EOL}${JSON.stringify(varsFromEnv)}`;
254+
when(platform.osType).thenReturn(osType.value);
255+
when(helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter)).thenResolve(cmd);
256+
when(processServiceFactory.create(resource)).thenResolve(instance(processService));
257+
when(envVarsService.getEnvironmentVariables(resource)).thenResolve({});
258+
when(processService.shellExec(anything(), anything())).thenResolve({ stdout: stdout });
259+
260+
const env = await service.getActivatedEnvironmentVariables(resource, interpreter);
261+
bustCache();
262+
const env2 = await service.getActivatedEnvironmentVariables(resource, interpreter);
263+
264+
expect(env).to.deep.equal(varsFromEnv);
265+
// Objects are different (not same reference).
266+
expect(env).to.not.equal(env2);
267+
// However variables are the same.
268+
expect(env).to.deep.equal(env2);
269+
270+
// All methods invoked twice as cache was blown.
271+
verify(helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter)).twice();
272+
verify(processServiceFactory.create(resource)).twice();
273+
verify(envVarsService.getEnvironmentVariables(resource)).twice();
274+
verify(processService.shellExec(anything(), anything())).twice();
275+
}
276+
test('Cache Variables get cleared when changing interpreter', async () => {
277+
await testClearingCache(onDidChangeInterpreter.fire.bind(onDidChangeInterpreter));
278+
});
279+
test('Cache Variables get cleared when changing env variables file', async () => {
280+
await testClearingCache(onDidChangeEnvVariables.fire.bind(onDidChangeEnvVariables));
281+
});
221282
});
222283
});
223284
});

0 commit comments

Comments
 (0)