Skip to content

Commit 262a325

Browse files
authored
Refactor formatters to use new execution framework (#426)
Fixes #352 Refactor code to use the new execution framework when launching processes
1 parent 660c3c5 commit 262a325

File tree

6 files changed

+96
-58
lines changed

6 files changed

+96
-58
lines changed

src/client/formatters/autoPep8Formatter.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,15 @@ export class AutoPep8Formatter extends BaseFormatter {
1515
public formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable<vscode.TextEdit[]> {
1616
const stopWatch = new StopWatch();
1717
const settings = PythonSettings.getInstance(document.uri);
18-
const autopep8Path = settings.formatting.autopep8Path;
19-
const autoPep8Args = Array.isArray(settings.formatting.autopep8Args) ? settings.formatting.autopep8Args : [];
20-
const hasCustomArgs = autoPep8Args.length > 0;
18+
const hasCustomArgs = Array.isArray(settings.formatting.autopep8Args) && settings.formatting.autopep8Args.length > 0;
2119
const formatSelection = range ? !range.isEmpty : false;
2220

23-
autoPep8Args.push('--diff');
21+
const autoPep8Args = ['--diff'];
2422
if (formatSelection) {
2523
// tslint:disable-next-line:no-non-null-assertion
2624
autoPep8Args.push(...['--line-range', (range!.start.line + 1).toString(), (range!.end.line + 1).toString()]);
2725
}
28-
const promise = super.provideDocumentFormattingEdits(document, options, token, autopep8Path, autoPep8Args);
26+
const promise = super.provideDocumentFormattingEdits(document, options, token, autoPep8Args);
2927
sendTelemetryWhenDone(FORMAT, promise, stopWatch, { tool: 'autoppep8', hasCustomArgs, formatSelection });
3028
return promise;
3129
}

src/client/formatters/baseFormatter.ts

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
33
import * as vscode from 'vscode';
4-
import { OutputChannel, Uri } from 'vscode';
4+
import { OutputChannel, TextEdit, Uri } from 'vscode';
55
import { STANDARD_OUTPUT_CHANNEL } from '../common/constants';
66
import { isNotInstalledError } from '../common/helpers';
7+
import { IProcessService, IPythonExecutionFactory } from '../common/process/types';
78
import { IInstaller, IOutputChannel, Product } from '../common/types';
9+
import { IEnvironmentVariablesProvider } from '../common/variables/types';
810
import { IServiceContainer } from '../ioc/types';
911
import { getTempFileWithDocumentContents, getTextEditsFromPatch } from './../common/editor';
10-
import { execPythonFile } from './../common/utils';
12+
import { IFormatterHelper } from './types';
1113

1214
export abstract class BaseFormatter {
1315
protected readonly outputChannel: OutputChannel;
16+
private readonly helper: IFormatterHelper;
1417
constructor(public Id: string, private product: Product, private serviceContainer: IServiceContainer) {
1518
this.outputChannel = this.serviceContainer.get<OutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL);
19+
this.helper = this.serviceContainer.get<IFormatterHelper>(IFormatterHelper);
1620
}
1721

1822
public abstract formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable<vscode.TextEdit[]>;
@@ -32,58 +36,72 @@ export abstract class BaseFormatter {
3236
}
3337
return vscode.Uri.file(__dirname);
3438
}
35-
protected provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, command: string, args: string[], cwd?: string): Thenable<vscode.TextEdit[]> {
39+
protected async provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, args: string[], cwd?: string): Promise<vscode.TextEdit[]> {
3640
this.outputChannel.clear();
3741
if (typeof cwd !== 'string' || cwd.length === 0) {
3842
cwd = this.getWorkspaceUri(document).fsPath;
3943
}
4044

41-
// autopep8 and yapf have the ability to read from the process input stream and return the formatted code out of the output stream
42-
// However they don't support returning the diff of the formatted text when reading data from the input stream
45+
// autopep8 and yapf have the ability to read from the process input stream and return the formatted code out of the output stream.
46+
// However they don't support returning the diff of the formatted text when reading data from the input stream.
4347
// Yes getting text formatted that way avoids having to create a temporary file, however the diffing will have
44-
// to be done here in node (extension), i.e. extension cpu, i.e. les responsive solution
48+
// to be done here in node (extension), i.e. extension cpu, i.e. les responsive solution.
4549
const tmpFileCreated = document.isDirty;
4650
const filePromise = tmpFileCreated ? getTempFileWithDocumentContents(document) : Promise.resolve(document.fileName);
47-
const promise = filePromise.then(filePath => {
48-
if (token && token.isCancellationRequested) {
49-
return [filePath, ''];
50-
}
51-
return Promise.all<string>([Promise.resolve(filePath), execPythonFile(document.uri, command, args.concat([filePath]), cwd!)]);
52-
}).then(data => {
53-
// Delete the temporary file created
54-
if (tmpFileCreated) {
55-
fs.unlink(data[0]);
56-
}
57-
if (token && token.isCancellationRequested) {
58-
return [];
59-
}
60-
return getTextEditsFromPatch(document.getText(), data[1]);
61-
}).catch(error => {
62-
this.handleError(this.Id, command, error, document.uri);
51+
const filePath = await filePromise;
52+
if (token && token.isCancellationRequested) {
6353
return [];
64-
});
54+
}
55+
56+
let executionPromise: Promise<string>;
57+
const executionInfo = this.helper.getExecutionInfo(this.product, args, document.uri);
58+
// Check if required to run as a module or executable.
59+
if (executionInfo.moduleName) {
60+
executionPromise = this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create(document.uri)
61+
.then(pythonExecutionService => pythonExecutionService.execModule(executionInfo.moduleName!, executionInfo.args.concat([filePath]), { cwd, throwOnStdErr: true, token }))
62+
.then(output => output.stdout);
63+
} else {
64+
const executionService = this.serviceContainer.get<IProcessService>(IProcessService);
65+
executionPromise = this.serviceContainer.get<IEnvironmentVariablesProvider>(IEnvironmentVariablesProvider).getEnvironmentVariables(true, document.uri)
66+
.then(env => executionService.exec(executionInfo.execPath!, executionInfo.args.concat([filePath]), { cwd, env, throwOnStdErr: true, token }))
67+
.then(output => output.stdout);
68+
}
69+
70+
const promise = executionPromise
71+
.then(data => {
72+
if (token && token.isCancellationRequested) {
73+
return [] as TextEdit[];
74+
}
75+
return getTextEditsFromPatch(document.getText(), data);
76+
})
77+
.catch(error => {
78+
if (token && token.isCancellationRequested) {
79+
return [] as TextEdit[];
80+
}
81+
// tslint:disable-next-line:no-empty
82+
this.handleError(this.Id, error, document.uri).catch(() => { });
83+
return [] as TextEdit[];
84+
})
85+
.then(edits => {
86+
// Delete the temporary file created
87+
if (tmpFileCreated) {
88+
fs.unlink(filePath);
89+
}
90+
return edits;
91+
});
6592
vscode.window.setStatusBarMessage(`Formatting with ${this.Id}`, promise);
6693
return promise;
6794
}
6895

69-
protected handleError(expectedFileName: string, fileName: string, error: Error, resource?: Uri) {
96+
protected async handleError(expectedFileName: string, error: Error, resource?: Uri) {
7097
let customError = `Formatting with ${this.Id} failed.`;
7198

7299
if (isNotInstalledError(error)) {
73-
// Check if we have some custom arguments such as "pylint --load-plugins pylint_django"
74-
// Such settings are no longer supported
75-
const stuffAfterFileName = fileName.substring(fileName.toUpperCase().lastIndexOf(expectedFileName) + expectedFileName.length);
76-
77-
// Ok if we have a space after the file name, this means we have some arguments defined and this isn't supported
78-
if (stuffAfterFileName.trim().indexOf(' ') > 0) {
79-
// tslint:disable-next-line:prefer-template
80-
customError = `Formatting failed, custom arguments in the 'python.formatting.${this.Id}Path' is not supported.\n` +
81-
`Custom arguments to the formatter can be defined in 'python.formatter.${this.Id}Args' setting of settings.json.`;
82-
} else {
83-
const installer = this.serviceContainer.get<IInstaller>(IInstaller);
100+
const installer = this.serviceContainer.get<IInstaller>(IInstaller);
101+
const isInstalled = await installer.isInstalled(this.product, resource);
102+
if (isInstalled) {
84103
customError += `\nYou could either install the '${this.Id}' formatter, turn it off or use another formatter.`;
85-
installer.promptToInstall(this.product, resource)
86-
.catch(ex => console.error('Python Extension: promptToInstall', ex));
104+
installer.promptToInstall(this.product, resource).catch(ex => console.error('Python Extension: promptToInstall', ex));
87105
}
88106
}
89107

src/client/formatters/helper.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
// Licensed under the MIT License.
33

44
import { injectable } from 'inversify';
5+
import * as path from 'path';
56
import 'reflect-metadata';
6-
import { IFormattingSettings } from '../common/configSettings';
7-
import { Product } from '../common/types';
7+
import { Uri } from 'vscode';
8+
import { IFormattingSettings, PythonSettings } from '../common/configSettings';
9+
import { ExecutionInfo, Product } from '../common/types';
810
import { FormatterId, FormatterSettingsPropertyNames, IFormatterHelper } from './types';
911

1012
@injectable()
@@ -25,4 +27,22 @@ export class FormatterHelper implements IFormatterHelper {
2527
pathName: `${id}Path` as keyof IFormattingSettings
2628
};
2729
}
30+
public getExecutionInfo(formatter: Product, customArgs: string[], resource?: Uri): ExecutionInfo {
31+
const settings = PythonSettings.getInstance(resource);
32+
const names = this.getSettingsPropertyNames(formatter);
33+
34+
const execPath = settings.formatting[names.pathName] as string;
35+
let args: string[] = Array.isArray(settings.formatting[names.argsName]) ? settings.formatting[names.argsName] as string[] : [];
36+
args = args.concat(customArgs);
37+
38+
let moduleName: string | undefined;
39+
40+
// If path information is not available, then treat it as a module,
41+
// except for prospector as that needs to be run as an executable (it's a Python package).
42+
if (path.basename(execPath) === execPath && formatter !== Product.prospector) {
43+
moduleName = execPath;
44+
}
45+
46+
return { execPath, moduleName, args };
47+
}
2848
}

src/client/formatters/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
import { Uri } from 'vscode';
45
import { IFormattingSettings } from '../common/configSettings';
5-
import { Product } from '../common/types';
6+
import { ExecutionInfo, Product } from '../common/types';
67

78
export const IFormatterHelper = Symbol('IFormatterHelper');
89

@@ -16,4 +17,5 @@ export type FormatterSettingsPropertyNames = {
1617
export interface IFormatterHelper {
1718
translateToId(formatter: Product): FormatterId;
1819
getSettingsPropertyNames(formatter: Product): FormatterSettingsPropertyNames;
20+
getExecutionInfo(formatter: Product, customArgs: string[], resource?: Uri): ExecutionInfo;
1921
}

src/client/formatters/yapfFormatter.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,18 @@ export class YapfFormatter extends BaseFormatter {
1515
public formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable<vscode.TextEdit[]> {
1616
const stopWatch = new StopWatch();
1717
const settings = PythonSettings.getInstance(document.uri);
18-
const yapfPath = settings.formatting.yapfPath;
19-
const yapfArgs = Array.isArray(settings.formatting.yapfArgs) ? settings.formatting.yapfArgs : [];
20-
const hasCustomArgs = yapfArgs.length > 0;
18+
const hasCustomArgs = Array.isArray(settings.formatting.yapfArgs) && settings.formatting.yapfArgs.length > 0;
2119
const formatSelection = range ? !range.isEmpty : false;
2220

23-
yapfArgs.push('--diff');
21+
const yapfArgs = ['--diff'];
2422
if (formatSelection) {
2523
// tslint:disable-next-line:no-non-null-assertion
2624
yapfArgs.push(...['--lines', `${range!.start.line + 1}-${range!.end.line + 1}`]);
2725
}
2826
// Yapf starts looking for config file starting from the file path.
2927
const fallbarFolder = this.getWorkspaceUri(document).fsPath;
3028
const cwd = this.getDocumentPath(document, fallbarFolder);
31-
const promise = super.provideDocumentFormattingEdits(document, options, token, yapfPath, yapfArgs, cwd);
29+
const promise = super.provideDocumentFormattingEdits(document, options, token, yapfArgs, cwd);
3230
sendTelemetryWhenDone(FORMAT, promise, stopWatch, { tool: 'yapf', hasCustomArgs, formatSelection });
3331
return promise;
3432
}

src/test/format/extension.format.test.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ import * as assert from 'assert';
22
import * as fs from 'fs-extra';
33
import * as path from 'path';
44
import * as vscode from 'vscode';
5-
import { CancellationTokenSource } from 'vscode';
6-
import { IProcessService } from '../../client/common/process/types';
7-
import { execPythonFile } from '../../client/common/utils';
5+
import { CancellationTokenSource, Uri } from 'vscode';
6+
import { IProcessService, IPythonExecutionFactory } from '../../client/common/process/types';
87
import { AutoPep8Formatter } from '../../client/formatters/autoPep8Formatter';
98
import { YapfFormatter } from '../../client/formatters/yapfFormatter';
109
import { closeActiveWindows, initialize, initializeTest } from '../initialize';
@@ -29,15 +28,17 @@ suite('Formatting', () => {
2928

3029
suiteSetup(async () => {
3130
await initialize();
31+
initializeDI();
3232
[autoPep8FileToFormat, autoPep8FileToAutoFormat, yapfFileToFormat, yapfFileToAutoFormat].forEach(file => {
3333
fs.copySync(originalUnformattedFile, file, { overwrite: true });
3434
});
3535
fs.ensureDirSync(path.dirname(autoPep8FileToFormat));
36-
const yapf = execPythonFile(workspaceRootPath, 'yapf', [originalUnformattedFile], workspaceRootPath, false);
37-
const autoPep8 = execPythonFile(workspaceRootPath, 'autopep8', [originalUnformattedFile], workspaceRootPath, false);
38-
await Promise.all<string>([yapf, autoPep8]).then(formattedResults => {
39-
formattedYapf = formattedResults[0];
40-
formattedAutoPep8 = formattedResults[1];
36+
const pythonProcess = await ioc.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create(Uri.file(workspaceRootPath));
37+
const yapf = pythonProcess.execModule('yapf', [originalUnformattedFile], { cwd: workspaceRootPath });
38+
const autoPep8 = pythonProcess.execModule('autopep8', [originalUnformattedFile], { cwd: workspaceRootPath });
39+
await Promise.all([yapf, autoPep8]).then(formattedResults => {
40+
formattedYapf = formattedResults[0].stdout;
41+
formattedAutoPep8 = formattedResults[1].stdout;
4142
});
4243
});
4344
setup(async () => {
@@ -63,6 +64,7 @@ suite('Formatting', () => {
6364
ioc.registerCommonTypes();
6465
ioc.registerVariableTypes();
6566
ioc.registerUnitTestTypes();
67+
ioc.registerFormatterTypes();
6668

6769
// Mocks.
6870
ioc.registerMockProcessTypes();

0 commit comments

Comments
 (0)