Skip to content

Commit 3e63746

Browse files
authored
Ensure sorting imports in a modified file picks up the proper configuration (#9128)
* Add support for passing stdin content to an exec'd process * Use stdin to pass Python source to isort This ensures that isort behaves consistently whether or not the file has modifications or has even been saved. * Add a news entry * Make these tests Windows compatible (hopefully) * Placate prettier * Test passing input to a launched STDIN * Document SpawnOptions.input * Clarify that the input stream will be closed if using SpawnOptions.input * Fix typo * Switch to handling input writing directly in the isort provider This changes from using .exec(...) to using .execObservable(...) and then interacting with the process more directly. * Remove now unused support for exec{,Observable} writing to stdin * Add logging to hopefully elucidate more information from Windows CI * Work around issues with isort seeking within stdin on Windows See comment for details. * System test for import sorting of modified files This validates that the configuration is picked up for these files. * Remove verbose logging lines from d92ceef This mostly reverts d92ceef, except for the extra test assertion it added which is still possibly useful.
1 parent afb23a3 commit 3e63746

5 files changed

Lines changed: 153 additions & 80 deletions

File tree

news/2 Fixes/4891.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Ensure sorting imports in a modified file picks up the proper configuration
2+
([#4891](https://github.com/Microsoft/vscode-python/issues/4891);
3+
thanks [Peter Law](https://github.com/PeterJCLaw))

pythonFiles/sortImports.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
33

4+
import io
45
import os
56
import os.path
67
import sys
78

89
isort_path = os.path.join(os.path.dirname(__file__), "lib", "python")
910
sys.path.insert(0, isort_path)
1011

12+
# Work around stdin buffering issues on windows (https://bugs.python.org/issue40540)
13+
# caused in part by isort seeking within the stdin stream by replacing the
14+
# stream with something which is definitely seekable.
15+
try:
16+
# python 3
17+
stdin = sys.stdin.buffer
18+
except AttributeError:
19+
# python 2
20+
stdin = sys.stdin
21+
22+
sys.stdin = io.BytesIO(stdin.read())
23+
# End workaround
24+
1125
import isort.main
1226

1327
isort.main.main()

src/client/providers/importSortProvider.ts

Lines changed: 62 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,16 @@ import { CancellationToken, TextDocument, Uri, WorkspaceEdit } from 'vscode';
55
import { IApplicationShell, ICommandManager, IDocumentManager } from '../common/application/types';
66
import { Commands, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from '../common/constants';
77
import { traceError } from '../common/logger';
8-
import { IFileSystem } from '../common/platform/types';
98
import * as internalScripts from '../common/process/internal/scripts';
10-
import { IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types';
9+
import { IProcessServiceFactory, IPythonExecutionFactory, ObservableExecutionResult } from '../common/process/types';
1110
import { IConfigurationService, IDisposableRegistry, IEditorUtils, IOutputChannel } from '../common/types';
11+
import { createDeferred } from '../common/utils/async';
1212
import { noop } from '../common/utils/misc';
1313
import { IServiceContainer } from '../ioc/types';
1414
import { captureTelemetry } from '../telemetry';
1515
import { EventName } from '../telemetry/constants';
1616
import { ISortImportsEditingProvider } from './types';
1717

18-
async function withRealFile<T>(
19-
document: TextDocument,
20-
fs: IFileSystem,
21-
useFile: (filename: string) => Promise<T>
22-
): Promise<[string, T]> {
23-
const filename = document.uri.fsPath;
24-
const text = document.getText();
25-
if (document.isDirty) {
26-
const tmpFile = await fs.createTemporaryFile(path.extname(filename));
27-
try {
28-
await fs.writeFile(tmpFile.filePath, text);
29-
const result = await useFile(tmpFile.filePath);
30-
return [text, result];
31-
} finally {
32-
tmpFile.dispose();
33-
}
34-
} else {
35-
const result = await useFile(filename);
36-
return [text, result];
37-
}
38-
}
39-
4018
@injectable()
4119
export class SortImportsEditingProvider implements ISortImportsEditingProvider {
4220
private readonly processServiceFactory: IProcessServiceFactory;
@@ -45,6 +23,7 @@ export class SortImportsEditingProvider implements ISortImportsEditingProvider {
4523
private readonly documentManager: IDocumentManager;
4624
private readonly configurationService: IConfigurationService;
4725
private readonly editorUtils: IEditorUtils;
26+
4827
public constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {
4928
this.shell = serviceContainer.get<IApplicationShell>(IApplicationShell);
5029
this.documentManager = serviceContainer.get<IDocumentManager>(IDocumentManager);
@@ -68,20 +47,14 @@ export class SortImportsEditingProvider implements ISortImportsEditingProvider {
6847
}
6948

7049
const execIsort = await this.getExecIsort(document, uri, token);
50+
if (token && token.isCancellationRequested) {
51+
return;
52+
}
53+
const diffPatch = await execIsort(document.getText());
7154

72-
// isort does have the ability to read from the process input stream and return the formatted code out of the output stream.
73-
// However they don't support returning the diff of the formatted text when reading data from the input stream.
74-
// Yes getting text formatted that way avoids having to create a temporary file, however the diffing will have
75-
// to be done here in node (extension), i.e. extension cpu, i.e. less responsive solution.
76-
const fs = this.serviceContainer.get<IFileSystem>(IFileSystem);
77-
const [text, diffPatch] = await withRealFile(document, fs, async (filename: string) => {
78-
if (token && token.isCancellationRequested) {
79-
return;
80-
}
81-
82-
return execIsort(filename);
83-
});
84-
return diffPatch ? this.editorUtils.getWorkspaceEditsFromPatch(text, diffPatch, document.uri) : undefined;
55+
return diffPatch
56+
? this.editorUtils.getWorkspaceEditsFromPatch(document.getText(), diffPatch, document.uri)
57+
: undefined;
8558
}
8659

8760
public registerCommands() {
@@ -129,29 +102,74 @@ export class SortImportsEditingProvider implements ISortImportsEditingProvider {
129102
}
130103
}
131104

132-
private async getExecIsort(document: TextDocument, uri: Uri, token?: CancellationToken) {
105+
private async getExecIsort(
106+
document: TextDocument,
107+
uri: Uri,
108+
token?: CancellationToken
109+
): Promise<(documentText: string) => Promise<string>> {
133110
const settings = this.configurationService.getSettings(uri);
134111
const _isort = settings.sortImports.path;
135112
const isort = typeof _isort === 'string' && _isort.length > 0 ? _isort : undefined;
136113
const isortArgs = settings.sortImports.args;
137114

115+
// We pass the content of the file to be sorted via stdin. This avoids
116+
// saving the file (as well as a potential temporary file), but does
117+
// mean that we need another way to tell `isort` where to look for
118+
// configuration. We do that by setting the working directory to the
119+
// directory which contains the file.
120+
const filename = '-';
121+
122+
const spawnOptions = {
123+
token,
124+
throwOnStdErr: true,
125+
cwd: path.dirname(uri.fsPath)
126+
};
127+
138128
if (isort) {
139129
const procService = await this.processServiceFactory.create(document.uri);
140130
// Use isort directly instead of the internal script.
141-
return async (filename: string) => {
131+
return async (documentText: string) => {
142132
const args = getIsortArgs(filename, isortArgs);
143-
const proc = await procService.exec(isort, args, { throwOnStdErr: true, token });
144-
return proc.stdout;
133+
const result = procService.execObservable(isort, args, spawnOptions);
134+
return this.communicateWithIsortProcess(result, documentText);
145135
};
146136
} else {
147137
const procService = await this.pythonExecutionFactory.create({ resource: document.uri });
148-
return async (filename: string) => {
138+
return async (documentText: string) => {
149139
const [args, parse] = internalScripts.sortImports(filename, isortArgs);
150-
const proc = await procService.exec(args, { throwOnStdErr: true, token });
151-
return parse(proc.stdout);
140+
const result = procService.execObservable(args, spawnOptions);
141+
return parse(await this.communicateWithIsortProcess(result, documentText));
152142
};
153143
}
154144
}
145+
146+
private async communicateWithIsortProcess(
147+
observableResult: ObservableExecutionResult<string>,
148+
inputText: string
149+
): Promise<string> {
150+
// Configure our listening to the output from isort ...
151+
let outputBuffer = '';
152+
const isortOutput = createDeferred<string>();
153+
observableResult.out.subscribe({
154+
next: (output) => {
155+
if (output.source === 'stdout') {
156+
outputBuffer += output.out;
157+
}
158+
},
159+
complete: () => {
160+
isortOutput.resolve(outputBuffer);
161+
}
162+
});
163+
164+
// ... then send isort the document content ...
165+
observableResult.proc?.stdin.write(inputText);
166+
observableResult.proc?.stdin.end();
167+
168+
// .. and finally wait for isort to do its thing
169+
await isortOutput.promise;
170+
171+
return outputBuffer;
172+
}
155173
}
156174

157175
function getIsortArgs(filename: string, extraArgs?: string[]): string[] {

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ suite('Sorting', () => {
107107
const textDocument = await workspace.openTextDocument(fileToFormatWithConfig);
108108
await window.showTextDocument(textDocument);
109109
const edit = (await sorter.provideDocumentSortImportsEdits(textDocument.uri))!;
110+
expect(edit).not.to.eq(undefined, 'No edit returned');
110111
expect(edit.entries()).to.be.lengthOf(1);
111112
const edits = edit.entries()[0][1];
112113
const newValue = `from third_party import lib2${EOL}from third_party import lib3${EOL}from third_party import lib4${EOL}from third_party import lib5${EOL}from third_party import lib6${EOL}from third_party import lib7${EOL}from third_party import lib8${EOL}from third_party import lib9${EOL}`;
@@ -158,4 +159,18 @@ suite('Sorting', () => {
158159
await commands.executeCommand(Commands.Sort_Imports);
159160
assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed');
160161
});
162+
163+
test('With Changes and Config implicit from cwd', async () => {
164+
const textDocument = await workspace.openTextDocument(fileToFormatWithConfig);
165+
assert.equal(textDocument.isDirty, false, 'Document should initially be unmodified');
166+
const editor = await window.showTextDocument(textDocument);
167+
await editor.edit((builder) => {
168+
builder.insert(new Position(0, 0), `from third_party import lib0${EOL}`);
169+
});
170+
assert.equal(textDocument.isDirty, true, 'Document should have been modified (pre sort)');
171+
await sorter.sortImports(textDocument.uri);
172+
assert.equal(textDocument.isDirty, true, 'Document should have been modified by sorting');
173+
const newValue = `from third_party import lib0${EOL}from third_party import lib1${EOL}from third_party import lib2${EOL}from third_party import lib3${EOL}from third_party import lib4${EOL}from third_party import lib5${EOL}from third_party import lib6${EOL}from third_party import lib7${EOL}from third_party import lib8${EOL}from third_party import lib9${EOL}`;
174+
assert.equal(textDocument.getText(), newValue);
175+
});
161176
});

0 commit comments

Comments
 (0)