Skip to content

Commit dfd7cf1

Browse files
author
Pavel Minaev
authored
Fix #14674: Enable overriding "pythonPath" in the launcher
Fix #12462: Update launch.json schema to add "python" and remove "pythonPath" Split the "pythonPath" debug property into "python", "debugAdapterPython", and "debugLauncherPython". Do most debug config validation on fully expanded property values via resolveDebugConfigurationWithSubstitutedVariables(). Add fixups for legacy launch.json with "pythonPath".
1 parent 171ffd0 commit dfd7cf1

File tree

17 files changed

+625
-231
lines changed

17 files changed

+625
-231
lines changed

news/1 Enhancements/12462.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Replaced "pythonPath" debug configuration property with "python".

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -623,9 +623,9 @@
623623
"description": "Absolute path to the program.",
624624
"default": "${file}"
625625
},
626-
"pythonPath": {
626+
"python": {
627627
"type": "string",
628-
"description": "Path (fully qualified) to python executable. Defaults to the value in settings",
628+
"description": "Absolute path to the Python interpreter executable; overrides workspace configuration if set.",
629629
"default": "${command:python.interpreterPath}"
630630
},
631631
"pythonArgs": {

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService {
7171
true
7272
);
7373
}
74+
7475
public async diagnose(resource: Resource): Promise<IDiagnostic[]> {
7576
if (!this.workspaceService.hasWorkspaceFolders) {
7677
return [];
@@ -80,9 +81,11 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService {
8081
: this.workspaceService.workspaceFolders![0];
8182
return this.diagnoseWorkspace(workspaceFolder, resource);
8283
}
84+
8385
protected async onHandle(diagnostics: IDiagnostic[]): Promise<void> {
8486
diagnostics.forEach((diagnostic) => this.handleDiagnostic(diagnostic));
8587
}
88+
8689
protected async fixLaunchJson(code: DiagnosticCodes) {
8790
if (!this.workspaceService.hasWorkspaceFolders) {
8891
return;
@@ -94,6 +97,7 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService {
9497
)
9598
);
9699
}
100+
97101
private async diagnoseWorkspace(workspaceFolder: WorkspaceFolder, resource: Resource) {
98102
const launchJson = this.getLaunchJsonFile(workspaceFolder);
99103
if (!(await this.fs.fileExists(launchJson))) {
@@ -114,6 +118,7 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService {
114118
diagnostics.push(new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConsoleTypeDiagnostic, resource));
115119
}
116120
if (
121+
fileContents.indexOf('"pythonPath":') > 0 ||
117122
fileContents.indexOf('{config:python.pythonPath}') > 0 ||
118123
fileContents.indexOf('{config:python.interpreterPath}') > 0
119124
) {
@@ -123,6 +128,7 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService {
123128
}
124129
return diagnostics;
125130
}
131+
126132
private async handleDiagnostic(diagnostic: IDiagnostic): Promise<void> {
127133
if (!this.canHandle(diagnostic)) {
128134
return;
@@ -147,6 +153,7 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService {
147153

148154
await this.messageService.handle(diagnostic, { commandPrompts });
149155
}
156+
150157
private async fixLaunchJsonInWorkspace(code: DiagnosticCodes, workspaceFolder: WorkspaceFolder) {
151158
if ((await this.diagnoseWorkspace(workspaceFolder, undefined)).length === 0) {
152159
return;
@@ -169,6 +176,7 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService {
169176
break;
170177
}
171178
case DiagnosticCodes.ConfigPythonPathDiagnostic: {
179+
fileContents = this.findAndReplace(fileContents, '"pythonPath":', '"python":');
172180
fileContents = this.findAndReplace(
173181
fileContents,
174182
'{config:python.pythonPath}',
@@ -188,10 +196,12 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService {
188196

189197
await this.fs.writeFile(launchJson, fileContents);
190198
}
199+
191200
private findAndReplace(fileContents: string, search: string, replace: string) {
192201
const searchRegex = new RegExp(search, 'g');
193202
return fileContents.replace(searchRegex, replace);
194203
}
204+
195205
private getLaunchJsonFile(workspaceFolder: WorkspaceFolder) {
196206
return path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json');
197207
}

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac
2727
@inject(IInterpreterService) private readonly interpreterService: IInterpreterService,
2828
@inject(IApplicationShell) private readonly appShell: IApplicationShell
2929
) {}
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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi
2929
private readonly providerFactory: IDebugConfigurationProviderFactory,
3030
@inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory
3131
) {}
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: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,36 @@ 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
3233
) {}
33-
public abstract resolveDebugConfiguration(
34+
35+
// This is a legacy hook used solely for backwards-compatible manual substitution
36+
// of ${command:python.interpreterPath} in "pythonPath", for the sake of other
37+
// existing implementations of resolveDebugConfiguration() that may rely on it.
38+
//
39+
// For all future config variables, expansion should be performed by VSCode itself,
40+
// and validation of debug configuration in derived classes should be performed in
41+
// resolveDebugConfigurationWithSubstitutedVariables() instead, where all variables
42+
// are already substituted.
43+
public async resolveDebugConfiguration(
44+
_folder: WorkspaceFolder | undefined,
45+
debugConfiguration: DebugConfiguration,
46+
_token?: CancellationToken
47+
): Promise<T | undefined> {
48+
return debugConfiguration as T;
49+
}
50+
51+
public abstract resolveDebugConfigurationWithSubstitutedVariables(
3452
folder: WorkspaceFolder | undefined,
3553
debugConfiguration: DebugConfiguration,
3654
token?: CancellationToken
3755
): Promise<T | undefined>;
56+
3857
protected getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined {
3958
if (folder) {
4059
return folder.uri;
@@ -56,19 +75,22 @@ export abstract class BaseConfigurationResolver<T extends DebugConfiguration>
5675
}
5776
}
5877
}
78+
5979
protected getProgram(): string | undefined {
6080
const editor = this.documentManager.activeTextEditor;
6181
if (editor && editor.document.languageId === PYTHON_LANGUAGE) {
6282
return editor.document.fileName;
6383
}
6484
}
85+
6586
protected resolveAndUpdatePaths(
6687
workspaceFolder: Uri | undefined,
6788
debugConfiguration: LaunchRequestArguments
6889
): void {
6990
this.resolveAndUpdateEnvFilePath(workspaceFolder, debugConfiguration);
7091
this.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration);
7192
}
93+
7294
protected resolveAndUpdateEnvFilePath(
7395
workspaceFolder: Uri | undefined,
7496
debugConfiguration: LaunchRequestArguments
@@ -84,6 +106,7 @@ export abstract class BaseConfigurationResolver<T extends DebugConfiguration>
84106
debugConfiguration.envFile = systemVariables.resolveAny(debugConfiguration.envFile);
85107
}
86108
}
109+
87110
protected resolveAndUpdatePythonPath(
88111
workspaceFolder: Uri | undefined,
89112
debugConfiguration: LaunchRequestArguments
@@ -99,16 +122,19 @@ export abstract class BaseConfigurationResolver<T extends DebugConfiguration>
99122
this.pythonPathSource = PythonPathSource.launchJson;
100123
}
101124
}
125+
102126
protected debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions) {
103127
if (debugOptions.indexOf(debugOption) >= 0) {
104128
return;
105129
}
106130
debugOptions.push(debugOption);
107131
}
132+
108133
protected isLocalHost(hostName?: string) {
109134
const LocalHosts = ['localhost', '127.0.0.1', '::1'];
110135
return hostName && LocalHosts.indexOf(hostName.toLowerCase()) >= 0 ? true : false;
111136
}
137+
112138
protected fixUpPathMappings(
113139
pathMappings: PathMapping[],
114140
defaultLocalRoot?: string,
@@ -153,9 +179,11 @@ export abstract class BaseConfigurationResolver<T extends DebugConfiguration>
153179

154180
return pathMappings;
155181
}
182+
156183
protected isDebuggingFlask(debugConfiguration: Partial<LaunchRequestArguments & AttachRequestArguments>) {
157184
return debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK' ? true : false;
158185
}
186+
159187
protected sendTelemetry(
160188
trigger: 'launch' | 'attach' | 'test',
161189
debugConfiguration: Partial<LaunchRequestArguments & AttachRequestArguments>

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

Lines changed: 53 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,18 @@ 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+
return (
186+
diagnosticService.validatePythonPath(debugConfiguration.python, this.pythonPathSource, folder?.uri) &&
187+
diagnosticService.validatePythonPath(
188+
debugConfiguration.debugAdapterPython,
189+
this.pythonPathSource,
190+
folder?.uri
191+
) &&
192+
diagnosticService.validatePythonPath(
193+
debugConfiguration.debugLauncherPython,
194+
this.pythonPathSource,
195+
folder?.uri
196+
)
167197
);
168198
}
169199
}

0 commit comments

Comments
 (0)