Skip to content

Commit ae5120a

Browse files
committed
Fix microsoft#14674: Enable overriding "pythonPath" in the launcher
Fix microsoft#12462: Update launch.json schema to add "python" and remove "pythonPath" Split the "pythonPath" debug property into "python", "debugAdapterPython", and "debugLauncherPython". Properly register the python.interpreterPath command to expand the ${command:interpreterPath} debug configuration variable. Do most debug config validation on fully expanded property values via resolveDebugConfigurationWithSubstitutedVariables(). Add fixups for legacy launch.json with "pythonPath" and/or "${command:python.interpreterPath}".
1 parent 98f5b49 commit ae5120a

File tree

14 files changed

+504
-235
lines changed

14 files changed

+504
-235
lines changed

package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1555,7 +1555,8 @@
15551555
"python"
15561556
],
15571557
"variables": {
1558-
"pickProcess": "python.pickLocalProcess"
1558+
"pickProcess": "python.pickLocalProcess",
1559+
"interpreterPath": "python.interpreterPath"
15591560
},
15601561
"configurationSnippets": [],
15611562
"configurationAttributes": {
@@ -1571,10 +1572,10 @@
15711572
"description": "Absolute path to the program.",
15721573
"default": "${file}"
15731574
},
1574-
"pythonPath": {
1575+
"python": {
15751576
"type": "string",
1576-
"description": "Path (fully qualified) to python executable. Defaults to the value in settings",
1577-
"default": "${command:python.interpreterPath}"
1577+
"description": "Absolute path to the Python interpreter executable; overrides workspace configuration if set.",
1578+
"default": "${command:interpreterPath}"
15781579
},
15791580
"pythonArgs": {
15801581
"type": "array",
@@ -3685,4 +3686,4 @@
36853686
"publisherDisplayName": "Microsoft",
36863687
"publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8"
36873688
}
3688-
}
3689+
}

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,10 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService {
114114
diagnostics.push(new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConsoleTypeDiagnostic, resource));
115115
}
116116
if (
117+
fileContents.indexOf('"pythonPath":') > 0 ||
117118
fileContents.indexOf('{config:python.pythonPath}') > 0 ||
118-
fileContents.indexOf('{config:python.interpreterPath}') > 0
119+
fileContents.indexOf('{config:python.interpreterPath}') > 0 ||
120+
fileContents.indexOf('{command:python.interpreterPath}') > 0
119121
) {
120122
diagnostics.push(
121123
new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConfigPythonPathDiagnostic, resource, false)
@@ -169,15 +171,21 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService {
169171
break;
170172
}
171173
case DiagnosticCodes.ConfigPythonPathDiagnostic: {
174+
fileContents = this.findAndReplace(fileContents, '"pythonPath":', '"python":');
172175
fileContents = this.findAndReplace(
173176
fileContents,
174177
'{config:python.pythonPath}',
175-
'{command:python.interpreterPath}'
178+
'{command:interpreterPath}'
176179
);
177180
fileContents = this.findAndReplace(
178181
fileContents,
179182
'{config:python.interpreterPath}',
180-
'{command:python.interpreterPath}'
183+
'{command:interpreterPath}'
184+
);
185+
fileContents = this.findAndReplace(
186+
fileContents,
187+
'{command:python.interpreterPath}',
188+
'{command:interpreterPath}'
181189
);
182190
break;
183191
}

src/client/debugger/extension/adapter/factory.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac
2626
constructor(
2727
@inject(IInterpreterService) private readonly interpreterService: IInterpreterService,
2828
@inject(IApplicationShell) private readonly appShell: IApplicationShell
29-
) {}
29+
) { }
30+
3031
public async createDebugAdapterDescriptor(
3132
session: DebugSession,
3233
_executable: DebugAdapterExecutable | undefined
@@ -54,7 +55,7 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac
5455
}
5556
}
5657

57-
const pythonPath = await this.getPythonPath(configuration, session.workspaceFolder);
58+
const pythonPath = await this.getDebugAdapterPython(configuration, session.workspaceFolder);
5859
if (pythonPath.length !== 0) {
5960
if (configuration.request === 'attach' && configuration.processId !== undefined) {
6061
sendTelemetryEvent(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS);
@@ -96,13 +97,16 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac
9697
* @returns {Promise<string>} Path to the python interpreter for this workspace.
9798
* @memberof DebugAdapterDescriptorFactory
9899
*/
99-
private async getPythonPath(
100+
private async getDebugAdapterPython(
100101
configuration: LaunchRequestArguments | AttachRequestArguments,
101102
workspaceFolder?: WorkspaceFolder
102103
): Promise<string> {
103-
if (configuration.pythonPath) {
104+
if (configuration.debugAdapterPython !== undefined) {
105+
return configuration.debugAdapterPython;
106+
} else if (configuration.pythonPath) {
104107
return configuration.pythonPath;
105108
}
109+
106110
const resourceUri = workspaceFolder ? workspaceFolder.uri : undefined;
107111
const interpreter = await this.interpreterService.getActiveInterpreter(resourceUri);
108112
if (interpreter) {

src/client/debugger/extension/configuration/debugConfigurationService.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi
2828
@inject(IDebugConfigurationProviderFactory)
2929
private readonly providerFactory: IDebugConfigurationProviderFactory,
3030
@inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory
31-
) {}
31+
) { }
32+
3233
public async provideDebugConfigurations(
3334
folder: WorkspaceFolder | undefined,
3435
token?: CancellationToken
@@ -46,6 +47,7 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi
4647
return [state.config as DebugConfiguration];
4748
}
4849
}
50+
4951
public async resolveDebugConfiguration(
5052
folder: WorkspaceFolder | undefined,
5153
debugConfiguration: DebugConfiguration,
@@ -76,6 +78,18 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi
7678
);
7779
}
7880
}
81+
82+
public async resolveDebugConfigurationWithSubstitutedVariables(
83+
folder: WorkspaceFolder | undefined,
84+
debugConfiguration: DebugConfiguration,
85+
token?: CancellationToken
86+
): Promise<DebugConfiguration | undefined> {
87+
function resolve<T extends DebugConfiguration>(resolver: IDebugConfigurationResolver<T>) {
88+
return resolver.resolveDebugConfigurationWithSubstitutedVariables(folder, debugConfiguration as T, token);
89+
}
90+
return debugConfiguration.request === 'attach' ? resolve(this.attachResolver) : resolve(this.launchResolver);
91+
}
92+
7993
protected async pickDebugConfiguration(
8094
input: IMultiStepInput<DebugConfigurationState>,
8195
state: DebugConfigurationState

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ export class AttachConfigurationResolver extends BaseConfigurationResolver<Attac
2121
) {
2222
super(workspaceService, documentManager, platformService, configurationService);
2323
}
24-
public async resolveDebugConfiguration(
24+
25+
public async resolveDebugConfigurationWithSubstitutedVariables(
2526
folder: WorkspaceFolder | undefined,
2627
debugConfiguration: AttachRequestArguments,
2728
_token?: CancellationToken
@@ -38,6 +39,7 @@ export class AttachConfigurationResolver extends BaseConfigurationResolver<Attac
3839
}
3940
return debugConfiguration;
4041
}
42+
4143
// tslint:disable-next-line:cyclomatic-complexity
4244
protected async provideAttachDefaults(
4345
workspaceFolder: Uri | undefined,

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

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,34 @@ import { IDebugConfigurationResolver } from '../types';
2424
export abstract class BaseConfigurationResolver<T extends DebugConfiguration>
2525
implements IDebugConfigurationResolver<T> {
2626
protected pythonPathSource: PythonPathSource = PythonPathSource.launchJson;
27+
2728
constructor(
2829
protected readonly workspaceService: IWorkspaceService,
2930
protected readonly documentManager: IDocumentManager,
3031
protected readonly platformService: IPlatformService,
3132
protected readonly configurationService: IConfigurationService
32-
) {}
33-
public abstract resolveDebugConfiguration(
33+
) { }
34+
35+
// This is a legacy hook used solely for backwards-compatible manual substitution
36+
// of ${command:python.interpreterPath} in "pythonPath". Newly added debug variables
37+
// should be properly registered in package.json, so that they're automatically
38+
// substituted by VSCode itself. All other validation in derived classes should be
39+
// performed in resolveDebugConfigurationWithSubstitutedVariables() instead, where
40+
// all variables are already substituted.
41+
public async resolveDebugConfiguration(
42+
_folder: WorkspaceFolder | undefined,
43+
debugConfiguration: DebugConfiguration,
44+
_token?: CancellationToken
45+
): Promise<T | undefined> {
46+
return debugConfiguration as T;
47+
}
48+
49+
public abstract resolveDebugConfigurationWithSubstitutedVariables(
3450
folder: WorkspaceFolder | undefined,
3551
debugConfiguration: DebugConfiguration,
3652
token?: CancellationToken
3753
): Promise<T | undefined>;
54+
3855
protected getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined {
3956
if (folder) {
4057
return folder.uri;
@@ -56,19 +73,22 @@ export abstract class BaseConfigurationResolver<T extends DebugConfiguration>
5673
}
5774
}
5875
}
76+
5977
protected getProgram(): string | undefined {
6078
const editor = this.documentManager.activeTextEditor;
6179
if (editor && editor.document.languageId === PYTHON_LANGUAGE) {
6280
return editor.document.fileName;
6381
}
6482
}
83+
6584
protected resolveAndUpdatePaths(
6685
workspaceFolder: Uri | undefined,
6786
debugConfiguration: LaunchRequestArguments
6887
): void {
6988
this.resolveAndUpdateEnvFilePath(workspaceFolder, debugConfiguration);
7089
this.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration);
7190
}
91+
7292
protected resolveAndUpdateEnvFilePath(
7393
workspaceFolder: Uri | undefined,
7494
debugConfiguration: LaunchRequestArguments
@@ -84,6 +104,7 @@ export abstract class BaseConfigurationResolver<T extends DebugConfiguration>
84104
debugConfiguration.envFile = systemVariables.resolveAny(debugConfiguration.envFile);
85105
}
86106
}
107+
87108
protected resolveAndUpdatePythonPath(
88109
workspaceFolder: Uri | undefined,
89110
debugConfiguration: LaunchRequestArguments
@@ -99,16 +120,19 @@ export abstract class BaseConfigurationResolver<T extends DebugConfiguration>
99120
this.pythonPathSource = PythonPathSource.launchJson;
100121
}
101122
}
123+
102124
protected debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions) {
103125
if (debugOptions.indexOf(debugOption) >= 0) {
104126
return;
105127
}
106128
debugOptions.push(debugOption);
107129
}
130+
108131
protected isLocalHost(hostName?: string) {
109132
const LocalHosts = ['localhost', '127.0.0.1', '::1'];
110133
return hostName && LocalHosts.indexOf(hostName.toLowerCase()) >= 0 ? true : false;
111134
}
135+
112136
protected fixUpPathMappings(
113137
pathMappings: PathMapping[],
114138
defaultLocalRoot?: string,
@@ -153,9 +177,11 @@ export abstract class BaseConfigurationResolver<T extends DebugConfiguration>
153177

154178
return pathMappings;
155179
}
180+
156181
protected isDebuggingFlask(debugConfiguration: Partial<LaunchRequestArguments & AttachRequestArguments>) {
157182
return debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK' ? true : false;
158183
}
184+
159185
protected sendTelemetry(
160186
trigger: 'launch' | 'attach' | 'test',
161187
debugConfiguration: Partial<LaunchRequestArguments & AttachRequestArguments>

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

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,47 +29,69 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver<Launc
2929
) {
3030
super(workspaceService, documentManager, platformService, configurationService);
3131
}
32+
3233
public async resolveDebugConfiguration(
3334
folder: WorkspaceFolder | undefined,
3435
debugConfiguration: LaunchRequestArguments,
3536
_token?: CancellationToken
3637
): Promise<LaunchRequestArguments | undefined> {
37-
const workspaceFolder = this.getWorkspaceFolder(folder);
38-
39-
const config = debugConfiguration as LaunchRequestArguments;
40-
const numberOfSettings = Object.keys(config);
41-
42-
if ((config.noDebug === true && numberOfSettings.length === 1) || numberOfSettings.length === 0) {
38+
if (
39+
debugConfiguration.name === undefined &&
40+
debugConfiguration.type === undefined &&
41+
debugConfiguration.request === undefined &&
42+
debugConfiguration.program === undefined &&
43+
debugConfiguration.env === undefined
44+
) {
4345
const defaultProgram = this.getProgram();
44-
45-
config.name = 'Launch';
46-
config.type = DebuggerTypeName;
47-
config.request = 'launch';
48-
config.program = defaultProgram ? defaultProgram : '';
49-
config.env = {};
46+
debugConfiguration.name = 'Launch';
47+
debugConfiguration.type = DebuggerTypeName;
48+
debugConfiguration.request = 'launch';
49+
debugConfiguration.program = defaultProgram ?? '';
50+
debugConfiguration.env = {};
5051
}
5152

52-
await this.provideLaunchDefaults(workspaceFolder, config);
53+
const workspaceFolder = this.getWorkspaceFolder(folder);
54+
this.resolveAndUpdatePaths(workspaceFolder, debugConfiguration);
55+
return debugConfiguration;
56+
}
5357

54-
const isValid = await this.validateLaunchConfiguration(folder, config);
58+
public async resolveDebugConfigurationWithSubstitutedVariables(
59+
folder: WorkspaceFolder | undefined,
60+
debugConfiguration: LaunchRequestArguments,
61+
_token?: CancellationToken
62+
): Promise<LaunchRequestArguments | undefined> {
63+
const workspaceFolder = this.getWorkspaceFolder(folder);
64+
await this.provideLaunchDefaults(workspaceFolder, debugConfiguration);
65+
66+
const isValid = await this.validateLaunchConfiguration(folder, debugConfiguration);
5567
if (!isValid) {
5668
return;
5769
}
5870

59-
const dbgConfig = debugConfiguration;
60-
if (Array.isArray(dbgConfig.debugOptions)) {
61-
dbgConfig.debugOptions = dbgConfig.debugOptions!.filter(
62-
(item, pos) => dbgConfig.debugOptions!.indexOf(item) === pos
71+
if (Array.isArray(debugConfiguration.debugOptions)) {
72+
debugConfiguration.debugOptions = debugConfiguration.debugOptions!.filter(
73+
(item, pos) => debugConfiguration.debugOptions!.indexOf(item) === pos
6374
);
6475
}
6576
return debugConfiguration;
6677
}
78+
6779
// tslint:disable-next-line:cyclomatic-complexity
6880
protected async provideLaunchDefaults(
6981
workspaceFolder: Uri | undefined,
7082
debugConfiguration: LaunchRequestArguments
7183
): Promise<void> {
72-
this.resolveAndUpdatePaths(workspaceFolder, debugConfiguration);
84+
if (debugConfiguration.python === undefined) {
85+
debugConfiguration.python = debugConfiguration.pythonPath;
86+
}
87+
if (debugConfiguration.debugAdapterPython === undefined) {
88+
debugConfiguration.debugAdapterPython = debugConfiguration.pythonPath;
89+
}
90+
if (debugConfiguration.debugLauncherPython === undefined) {
91+
debugConfiguration.debugLauncherPython = debugConfiguration.pythonPath;
92+
}
93+
delete debugConfiguration.pythonPath;
94+
7395
if (typeof debugConfiguration.cwd !== 'string' && workspaceFolder) {
7496
debugConfiguration.cwd = workspaceFolder.fsPath;
7597
}
@@ -160,10 +182,19 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver<Launc
160182
debugConfiguration: LaunchRequestArguments
161183
): Promise<boolean> {
162184
const diagnosticService = this.invalidPythonPathInDebuggerService;
163-
return diagnosticService.validatePythonPath(
164-
debugConfiguration.pythonPath,
165-
this.pythonPathSource,
166-
folder ? folder.uri : undefined
185+
const resource = folder ? folder.uri : undefined;
186+
return (
187+
diagnosticService.validatePythonPath(debugConfiguration.python, this.pythonPathSource, resource) &&
188+
diagnosticService.validatePythonPath(
189+
debugConfiguration.debugAdapterPython,
190+
this.pythonPathSource,
191+
resource
192+
) &&
193+
diagnosticService.validatePythonPath(
194+
debugConfiguration.debugLauncherPython,
195+
this.pythonPathSource,
196+
resource
197+
)
167198
);
168199
}
169200
}

src/client/debugger/extension/configuration/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ export interface IDebugConfigurationResolver<T extends DebugConfiguration> {
1313
debugConfiguration: T,
1414
token?: CancellationToken
1515
): Promise<T | undefined>;
16+
17+
resolveDebugConfigurationWithSubstitutedVariables(
18+
folder: WorkspaceFolder | undefined,
19+
debugConfiguration: T,
20+
token?: CancellationToken
21+
): Promise<T | undefined>;
1622
}
1723

1824
export const IDebugConfigurationProviderFactory = Symbol('IDebugConfigurationProviderFactory');

0 commit comments

Comments
 (0)