Skip to content

Commit bcac5c1

Browse files
author
Kartik Raj
authored
Use ${command:python.interpreterPath} to get selected interpreter path in launch.json and tasks.json (#11840)
* Added implementation * Replace config with command * Added migrations * Added tests * Added tests * Undo * News entry * Remove variable
1 parent 1c16780 commit bcac5c1

File tree

15 files changed

+176
-14
lines changed

15 files changed

+176
-14
lines changed

news/2 Fixes/11789.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Use `${command:python.interpreterPath}` to get selected interpreter path in `launch.json` and `tasks.json`.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,7 +1212,7 @@
12121212
"pythonPath": {
12131213
"type": "string",
12141214
"description": "Path (fully qualified) to python executable. Defaults to the value in settings",
1215-
"default": "${config:python.interpreterPath}"
1215+
"default": "${command:python.interpreterPath}"
12161216
},
12171217
"args": {
12181218
"type": "array",
@@ -1350,7 +1350,7 @@
13501350
"pythonPath": {
13511351
"type": "string",
13521352
"description": "Path (fully qualified) to python executable. Defaults to the value in settings",
1353-
"default": "${config:python.interpreterPath}"
1353+
"default": "${command:python.interpreterPath}"
13541354
},
13551355
"stopOnEntry": {
13561356
"type": "boolean",

src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,10 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService {
113113
if (fileContents.indexOf('"console": "none"') > 0) {
114114
diagnostics.push(new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConsoleTypeDiagnostic, resource));
115115
}
116-
if (fileContents.indexOf('{config:python.pythonPath}') > 0) {
116+
if (
117+
fileContents.indexOf('{config:python.pythonPath}') > 0 ||
118+
fileContents.indexOf('{config:python.interpreterPath}') > 0
119+
) {
117120
diagnostics.push(
118121
new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConfigPythonPathDiagnostic, resource, false)
119122
);
@@ -169,7 +172,12 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService {
169172
fileContents = this.findAndReplace(
170173
fileContents,
171174
'{config:python.pythonPath}',
172-
'{config:python.interpreterPath}'
175+
'{command:python.interpreterPath}'
176+
);
177+
fileContents = this.findAndReplace(
178+
fileContents,
179+
'{config:python.interpreterPath}',
180+
'{command:python.interpreterPath}'
173181
);
174182
break;
175183
}

src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export class InvalidPythonPathInDebuggerService extends BaseDiagnosticsService
7676
public async validatePythonPath(pythonPath?: string, pythonPathSource?: PythonPathSource, resource?: Uri) {
7777
pythonPath = pythonPath ? this.resolveVariables(pythonPath, resource) : undefined;
7878
// tslint:disable-next-line:no-invalid-template-strings
79-
if (pythonPath === '${config:python.interpreterPath}' || !pythonPath) {
79+
if (pythonPath === '${command:python.interpreterPath}' || !pythonPath) {
8080
pythonPath = this.configService.getSettings(resource).pythonPath;
8181
}
8282
if (await this.interpreterHelper.getInterpreterInformation(pythonPath).catch(() => undefined)) {

src/client/common/application/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu
9494
['vscode.open']: [Uri];
9595
['workbench.action.files.saveAs']: [Uri];
9696
['workbench.action.files.save']: [Uri];
97+
[Commands.GetSelectedInterpreterPath]: [{ workspaceFolder: string } | string[]];
9798
[Commands.Build_Workspace_Symbols]: [boolean, CancellationToken];
9899
[Commands.Sort_Imports]: [undefined, Uri];
99100
[Commands.Exec_In_Terminal]: [undefined, Uri];

src/client/common/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export namespace Commands {
6363
export const SwitchToInsidersDaily = 'python.switchToDailyChannel';
6464
export const SwitchToInsidersWeekly = 'python.switchToWeeklyChannel';
6565
export const PickLocalProcess = 'python.pickLocalProcess';
66+
export const GetSelectedInterpreterPath = 'python.interpreterPath';
6667
export const ClearWorkspaceInterpreter = 'python.clearWorkspaceInterpreter';
6768
export const ResetInterpreterSecurityStorage = 'python.resetInterpreterSecurityStorage';
6869
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { inject, injectable } from 'inversify';
7+
import { Uri } from 'vscode';
8+
import { IExtensionSingleActivationService } from '../../../../activation/types';
9+
import { ICommandManager } from '../../../../common/application/types';
10+
import { Commands } from '../../../../common/constants';
11+
import { IConfigurationService, IDisposable, IDisposableRegistry } from '../../../../common/types';
12+
13+
@injectable()
14+
export class InterpreterPathCommand implements IExtensionSingleActivationService {
15+
constructor(
16+
@inject(ICommandManager) private readonly commandManager: ICommandManager,
17+
@inject(IConfigurationService) private readonly configurationService: IConfigurationService,
18+
@inject(IDisposableRegistry) private readonly disposables: IDisposable[]
19+
) {}
20+
21+
public async activate() {
22+
this.disposables.push(
23+
this.commandManager.registerCommand(Commands.GetSelectedInterpreterPath, (args) => {
24+
return this._getSelectedInterpreterPath(args);
25+
})
26+
);
27+
}
28+
29+
public _getSelectedInterpreterPath(args: { workspaceFolder: string } | string[]): string {
30+
// If `launch.json` is launching this command, `args.workspaceFolder` carries the workspaceFolder
31+
// If `tasks.json` is launching this command, `args[1]` carries the workspaceFolder
32+
const workspaceFolder = 'workspaceFolder' in args ? args.workspaceFolder : args[1] ? args[1] : undefined;
33+
return this.configurationService.getSettings(workspaceFolder ? Uri.parse(workspaceFolder) : undefined)
34+
.pythonPath;
35+
}
36+
}

src/client/debugger/extension/configuration/resolvers/base.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export abstract class BaseConfigurationResolver<T extends DebugConfiguration>
9191
if (!debugConfiguration) {
9292
return;
9393
}
94-
if (debugConfiguration.pythonPath === '${config:python.interpreterPath}' || !debugConfiguration.pythonPath) {
94+
if (debugConfiguration.pythonPath === '${command:python.interpreterPath}' || !debugConfiguration.pythonPath) {
9595
const pythonPath = this.configurationService.getSettings(workspaceFolder).pythonPath;
9696
debugConfiguration.pythonPath = pythonPath;
9797
this.pythonPathSource = PythonPathSource.settingsJson;

src/client/debugger/extension/serviceRegistry.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { IAttachProcessProviderFactory } from './attachQuickPick/types';
1515
import { DebuggerBanner } from './banner';
1616
import { PythonDebugConfigurationService } from './configuration/debugConfigurationService';
1717
import { LaunchJsonCompletionProvider } from './configuration/launch.json/completionProvider';
18+
import { InterpreterPathCommand } from './configuration/launch.json/interpreterPathCommand';
1819
import { LaunchJsonUpdaterService } from './configuration/launch.json/updaterService';
1920
import { DjangoLaunchDebugConfigurationProvider } from './configuration/providers/djangoLaunch';
2021
import { FileLaunchDebugConfigurationProvider } from './configuration/providers/fileLaunch';
@@ -51,6 +52,10 @@ export function registerTypes(serviceManager: IServiceManager) {
5152
IExtensionSingleActivationService,
5253
LaunchJsonCompletionProvider
5354
);
55+
serviceManager.addSingleton<IExtensionSingleActivationService>(
56+
IExtensionSingleActivationService,
57+
InterpreterPathCommand
58+
);
5459
serviceManager.addSingleton<IExtensionSingleActivationService>(
5560
IExtensionSingleActivationService,
5661
LaunchJsonUpdaterService

src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,31 @@ suite('Application Diagnostics - Checks if launch.json is invalid', () => {
218218
fs.verifyAll();
219219
});
220220

221+
test('Should return ConfigPythonPathDiagnostic if file launch.json contains string "{config:python.interpreterPath}"', async () => {
222+
const fileContents = 'Hello I am launch.json, I contain string {config:python.interpreterPath}';
223+
workspaceService
224+
.setup((w) => w.hasWorkspaceFolders)
225+
.returns(() => true)
226+
.verifiable(TypeMoq.Times.once());
227+
workspaceService
228+
.setup((w) => w.workspaceFolders)
229+
.returns(() => [workspaceFolder])
230+
.verifiable(TypeMoq.Times.once());
231+
fs.setup((w) => w.fileExists(TypeMoq.It.isAny()))
232+
.returns(() => Promise.resolve(true))
233+
.verifiable(TypeMoq.Times.once());
234+
fs.setup((w) => w.readFile(TypeMoq.It.isAny()))
235+
.returns(() => Promise.resolve(fileContents))
236+
.verifiable(TypeMoq.Times.once());
237+
const diagnostics = await diagnosticService.diagnose(undefined);
238+
expect(diagnostics).to.be.deep.equal(
239+
[new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConfigPythonPathDiagnostic, undefined, false)],
240+
'Diagnostics returned are not as expected'
241+
);
242+
workspaceService.verifyAll();
243+
fs.verifyAll();
244+
});
245+
221246
test('Should return both diagnostics if file launch.json contains string "debugStdLib" and "pythonExperimental"', async () => {
222247
const fileContents = 'Hello I am launch.json, I contain both "debugStdLib" and "pythonExperimental"';
223248
workspaceService
@@ -471,8 +496,9 @@ suite('Application Diagnostics - Checks if launch.json is invalid', () => {
471496
});
472497

473498
test('File launch.json is fixed correctly when code equals ConfigPythonPathDiagnostic ', async () => {
474-
const launchJson = 'This string contains {config:python.pythonPath}';
475-
const correctedlaunchJson = 'This string contains {config:python.interpreterPath}';
499+
const launchJson = 'This string contains {config:python.pythonPath} & {config:python.interpreterPath}';
500+
const correctedlaunchJson =
501+
'This string contains {command:python.interpreterPath} & {command:python.interpreterPath}';
476502
workspaceService
477503
.setup((w) => w.hasWorkspaceFolders)
478504
.returns(() => true)

src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,8 @@ suite('Application Diagnostics - Checks Python Path in debugger', () => {
227227
expect(options!.commandPrompts).to.be.lengthOf(1);
228228
expect(options!.commandPrompts[0].prompt).to.be.equal('Open launch.json');
229229
});
230-
test('Ensure we get python path from config when path = ${config:python.interpreterPath}', async () => {
231-
const pythonPath = '${config:python.interpreterPath}';
230+
test('Ensure we get python path from config when path = ${command:python.interpreterPath}', async () => {
231+
const pythonPath = '${command:python.interpreterPath}';
232232

233233
const settings = typemoq.Mock.ofType<IPythonSettings>();
234234
settings
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { assert, expect } from 'chai';
7+
import * as sinon from 'sinon';
8+
import { anything, instance, mock, verify, when } from 'ts-mockito';
9+
import * as TypeMoq from 'typemoq';
10+
import { Uri } from 'vscode';
11+
import { CommandManager } from '../../../../../client/common/application/commandManager';
12+
import { ICommandManager } from '../../../../../client/common/application/types';
13+
import { ConfigurationService } from '../../../../../client/common/configuration/service';
14+
import { Commands } from '../../../../../client/common/constants';
15+
import { IConfigurationService, IDisposable } from '../../../../../client/common/types';
16+
import { InterpreterPathCommand } from '../../../../../client/debugger/extension/configuration/launch.json/interpreterPathCommand';
17+
18+
suite('Interpreter Path Command', () => {
19+
let cmdManager: ICommandManager;
20+
let configService: IConfigurationService;
21+
let interpreterPathCommand: InterpreterPathCommand;
22+
setup(() => {
23+
cmdManager = mock(CommandManager);
24+
configService = mock(ConfigurationService);
25+
interpreterPathCommand = new InterpreterPathCommand(instance(cmdManager), instance(configService), []);
26+
});
27+
28+
teardown(() => {
29+
sinon.restore();
30+
});
31+
32+
test('Ensure command is registered with the correct callback handler', async () => {
33+
let getInterpreterPathHandler!: Function;
34+
when(cmdManager.registerCommand(Commands.GetSelectedInterpreterPath, anything())).thenCall((_, cb) => {
35+
getInterpreterPathHandler = cb;
36+
return TypeMoq.Mock.ofType<IDisposable>().object;
37+
});
38+
39+
await interpreterPathCommand.activate();
40+
41+
verify(cmdManager.registerCommand(Commands.GetSelectedInterpreterPath, anything())).once();
42+
43+
const getSelectedInterpreterPath = sinon.stub(InterpreterPathCommand.prototype, '_getSelectedInterpreterPath');
44+
getInterpreterPathHandler([]);
45+
assert(getSelectedInterpreterPath.calledOnceWith([]));
46+
});
47+
48+
test('If `workspaceFolder` property exists in `args`, it is used to retrieve setting from config', async () => {
49+
const args = { workspaceFolder: 'folderPath' };
50+
when(configService.getSettings(anything())).thenCall((arg) => {
51+
assert.deepEqual(arg, Uri.parse('folderPath'));
52+
// tslint:disable-next-line: no-any
53+
return { pythonPath: 'settingValue' } as any;
54+
});
55+
const setting = interpreterPathCommand._getSelectedInterpreterPath(args);
56+
expect(setting).to.equal('settingValue');
57+
});
58+
59+
test('If `args[1]` is defined, it is used to retrieve setting from config', async () => {
60+
const args = ['command', 'folderPath'];
61+
when(configService.getSettings(anything())).thenCall((arg) => {
62+
assert.deepEqual(arg, Uri.parse('folderPath'));
63+
// tslint:disable-next-line: no-any
64+
return { pythonPath: 'settingValue' } as any;
65+
});
66+
const setting = interpreterPathCommand._getSelectedInterpreterPath(args);
67+
expect(setting).to.equal('settingValue');
68+
});
69+
70+
test('If neither of these exists, value of workspace folder is `undefined`', async () => {
71+
const args = ['command'];
72+
// tslint:disable-next-line: no-any
73+
when(configService.getSettings(undefined)).thenReturn({ pythonPath: 'settingValue' } as any);
74+
const setting = interpreterPathCommand._getSelectedInterpreterPath(args);
75+
expect(setting).to.equal('settingValue');
76+
});
77+
});

src/test/debugger/extension/configuration/resolvers/base.unit.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,9 @@ suite('Debugging - Config Resolver', () => {
195195

196196
expect(config).to.have.property('pythonPath', pythonPath);
197197
});
198-
test('Python path in debug config must point to pythonpath in settings if pythonPath in config is ${config:python.interpreterPath}', () => {
198+
test('Python path in debug config must point to pythonpath in settings if pythonPath in config is ${command:python.interpreterPath}', () => {
199199
const config = {
200-
pythonPath: '${config:python.interpreterPath}'
200+
pythonPath: '${command:python.interpreterPath}'
201201
};
202202
const pythonPath = path.join('1', '2', '3');
203203

src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => {
485485
}
486486
]);
487487
});
488-
test('Ensure `${config:python.interpreterPath}` is replaced with actual pythonPath', async () => {
488+
test('Ensure `${command:python.interpreterPath}` is replaced with actual pythonPath', async () => {
489489
const pythonPath = `PythonPath_${new Date().toString()}`;
490490
const activeFile = 'xyz.py';
491491
const workspaceFolder = createMoqWorkspaceFolder(__dirname);
@@ -495,7 +495,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => {
495495
setupWorkspaces([defaultWorkspace]);
496496

497497
const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({
498-
pythonPath: '${config:python.interpreterPath}'
498+
pythonPath: '${command:python.interpreterPath}'
499499
} as any) as DebugConfiguration);
500500

501501
expect(debugConfig).to.have.property('pythonPath', pythonPath);

src/test/debugger/extension/serviceRegistry.unit.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { IAttachProcessProviderFactory } from '../../../client/debugger/extensio
1616
import { DebuggerBanner } from '../../../client/debugger/extension/banner';
1717
import { PythonDebugConfigurationService } from '../../../client/debugger/extension/configuration/debugConfigurationService';
1818
import { LaunchJsonCompletionProvider } from '../../../client/debugger/extension/configuration/launch.json/completionProvider';
19+
import { InterpreterPathCommand } from '../../../client/debugger/extension/configuration/launch.json/interpreterPathCommand';
1920
import { LaunchJsonUpdaterService } from '../../../client/debugger/extension/configuration/launch.json/updaterService';
2021
import { DjangoLaunchDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/djangoLaunch';
2122
import { FileLaunchDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/fileLaunch';
@@ -59,6 +60,12 @@ suite('Debugging - Service Registry', () => {
5960
test('Registrations', () => {
6061
registerTypes(instance(serviceManager));
6162

63+
verify(
64+
serviceManager.addSingleton<IExtensionSingleActivationService>(
65+
IExtensionSingleActivationService,
66+
InterpreterPathCommand
67+
)
68+
).once();
6269
verify(
6370
serviceManager.addSingleton<IDebugConfigurationService>(
6471
IDebugConfigurationService,

0 commit comments

Comments
 (0)