Skip to content

Commit 6489528

Browse files
author
Kartik Raj
authored
Show a general warning prompt pointing to the upgrade guide when users attempt to run isort5 using deprecated settings (microsoft#13717)
* Show a general warning prompt pointing to the upgrade guide when users attempt to run isort5 using deprecated settings * Code reviews
1 parent 9169202 commit 6489528

File tree

5 files changed

+193
-6
lines changed

5 files changed

+193
-6
lines changed

news/1 Enhancements/13716.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Show a general warning prompt pointing to the upgrade guide when users attempt to run isort5 using deprecated settings.

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@
306306
"diagnostics.invalidDebuggerTypeDiagnostic": "Your launch.json file needs to be updated to change the \"pythonExperimental\" debug configurations to use the \"python\" debugger type, otherwise Python debugging may not work. Would you like to automatically update your launch.json file now?",
307307
"diagnostics.consoleTypeDiagnostic": "Your launch.json file needs to be updated to change the console type string from \"none\" to \"internalConsole\", otherwise Python debugging may not work. Would you like to automatically update your launch.json file now?",
308308
"diagnostics.justMyCodeDiagnostic": "Configuration \"debugStdLib\" in launch.json is no longer supported. It's recommended to replace it with \"justMyCode\", which is the exact opposite of using \"debugStdLib\". Would you like to automatically update your launch.json file to do that?",
309+
"diagnostics.checkIsort5UpgradeGuide": "We found outdated configuration for sorting imports in this workspace. Check the [isort upgrade guide](https://aka.ms/AA9j5x4) to update your settings.",
309310
"diagnostics.yesUpdateLaunch": "Yes, update launch.json",
310311
"diagnostics.invalidTestSettings": "Your settings needs to be updated to change the setting \"python.unitTest.\" to \"python.testing.\", otherwise testing Python code using the extension may not work. Would you like to automatically update your settings now?",
311312
"DataScience.interruptKernel": "Interrupt IPython kernel",

src/client/common/utils/localize.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ export namespace Diagnostics {
6060
'Your settings needs to be updated to change the setting "python.unitTest." to "python.testing.", otherwise testing Python code using the extension may not work. Would you like to automatically update your settings now?'
6161
);
6262
export const updateSettings = localize('diagnostics.updateSettings', 'Yes, update settings');
63+
export const checkIsort5UpgradeGuide = localize(
64+
'diagnostics.checkIsort5UpgradeGuide',
65+
'We found outdated configuration for sorting imports in this workspace. Check the [isort upgrade guide](https://aka.ms/AA9j5x4) to update your settings.'
66+
);
6367
}
6468

6569
export namespace Common {

src/client/providers/importSortProvider.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,23 @@ import { Commands, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from '../common/co
77
import { traceError } from '../common/logger';
88
import * as internalScripts from '../common/process/internal/scripts';
99
import { IProcessServiceFactory, IPythonExecutionFactory, ObservableExecutionResult } from '../common/process/types';
10-
import { IConfigurationService, IDisposableRegistry, IEditorUtils, IOutputChannel } from '../common/types';
10+
import {
11+
IConfigurationService,
12+
IDisposableRegistry,
13+
IEditorUtils,
14+
IOutputChannel,
15+
IPersistentStateFactory
16+
} from '../common/types';
1117
import { createDeferred, createDeferredFromPromise, Deferred } from '../common/utils/async';
18+
import { Common, Diagnostics } from '../common/utils/localize';
1219
import { noop } from '../common/utils/misc';
1320
import { IServiceContainer } from '../ioc/types';
1421
import { captureTelemetry } from '../telemetry';
1522
import { EventName } from '../telemetry/constants';
1623
import { ISortImportsEditingProvider } from './types';
1724

25+
const doNotDisplayPromptStateKey = 'ISORT5_UPGRADE_WARNING_KEY';
26+
1827
@injectable()
1928
export class SortImportsEditingProvider implements ISortImportsEditingProvider {
2029
private readonly isortPromises = new Map<
@@ -24,9 +33,11 @@ export class SortImportsEditingProvider implements ISortImportsEditingProvider {
2433
private readonly processServiceFactory: IProcessServiceFactory;
2534
private readonly pythonExecutionFactory: IPythonExecutionFactory;
2635
private readonly shell: IApplicationShell;
36+
private readonly persistentStateFactory: IPersistentStateFactory;
2737
private readonly documentManager: IDocumentManager;
2838
private readonly configurationService: IConfigurationService;
2939
private readonly editorUtils: IEditorUtils;
40+
private readonly output: IOutputChannel;
3041

3142
public constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {
3243
this.shell = serviceContainer.get<IApplicationShell>(IApplicationShell);
@@ -35,6 +46,8 @@ export class SortImportsEditingProvider implements ISortImportsEditingProvider {
3546
this.pythonExecutionFactory = serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory);
3647
this.processServiceFactory = serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory);
3748
this.editorUtils = serviceContainer.get<IEditorUtils>(IEditorUtils);
49+
this.output = serviceContainer.get<IOutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL);
50+
this.persistentStateFactory = serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory);
3851
}
3952

4053
@captureTelemetry(EventName.FORMAT_SORT_IMPORTS)
@@ -122,6 +135,26 @@ export class SortImportsEditingProvider implements ISortImportsEditingProvider {
122135
}
123136
}
124137

138+
public async _showWarningAndOptionallyShowOutput() {
139+
const neverShowAgain = this.persistentStateFactory.createGlobalPersistentState(
140+
doNotDisplayPromptStateKey,
141+
false
142+
);
143+
if (neverShowAgain.value) {
144+
return;
145+
}
146+
const selection = await this.shell.showWarningMessage(
147+
Diagnostics.checkIsort5UpgradeGuide(),
148+
Common.openOutputPanel(),
149+
Common.doNotShowAgain()
150+
);
151+
if (selection === Common.openOutputPanel()) {
152+
this.output.show(true);
153+
} else if (selection === Common.doNotShowAgain()) {
154+
await neverShowAgain.updateValue(true);
155+
}
156+
}
157+
125158
private async getExecIsort(
126159
document: TextDocument,
127160
uri: Uri,
@@ -141,7 +174,6 @@ export class SortImportsEditingProvider implements ISortImportsEditingProvider {
141174

142175
const spawnOptions = {
143176
token,
144-
throwOnStdErr: true,
145177
cwd: path.dirname(uri.fsPath)
146178
};
147179

@@ -169,11 +201,20 @@ export class SortImportsEditingProvider implements ISortImportsEditingProvider {
169201
): Promise<string> {
170202
// Configure our listening to the output from isort ...
171203
let outputBuffer = '';
204+
let isAnyErrorRelatedToUpgradeGuide = false;
172205
const isortOutput = createDeferred<string>();
173206
observableResult.out.subscribe({
174207
next: (output) => {
175208
if (output.source === 'stdout') {
176209
outputBuffer += output.out;
210+
} else {
211+
// All the W0500 warning codes point to isort5 upgrade guide: https://pycqa.github.io/isort/docs/warning_and_error_codes/W0500/
212+
// Do not throw error on these types of stdErrors
213+
isAnyErrorRelatedToUpgradeGuide = isAnyErrorRelatedToUpgradeGuide || output.out.includes('W050');
214+
traceError(output.out);
215+
if (!output.out.includes('W050')) {
216+
isortOutput.reject(output.out);
217+
}
177218
}
178219
},
179220
complete: () => {
@@ -188,6 +229,9 @@ export class SortImportsEditingProvider implements ISortImportsEditingProvider {
188229
// .. and finally wait for isort to do its thing
189230
await isortOutput.promise;
190231

232+
if (isAnyErrorRelatedToUpgradeGuide) {
233+
this._showWarningAndOptionallyShowOutput().ignoreErrors();
234+
}
191235
return outputBuffer;
192236
}
193237
}

src/test/providers/importSortProvider.unit.test.ts

Lines changed: 141 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@
55

66
// tslint:disable:no-any max-func-body-length
77

8-
import { expect } from 'chai';
8+
import { assert, expect } from 'chai';
99
import { ChildProcess } from 'child_process';
1010
import { EOL } from 'os';
1111
import * as path from 'path';
1212
import { Observable } from 'rxjs/Observable';
1313
import { Subscriber } from 'rxjs/Subscriber';
14+
import * as sinon from 'sinon';
1415
import { Writable } from 'stream';
1516
import * as TypeMoq from 'typemoq';
1617
import { Range, TextDocument, TextEditor, TextLine, Uri, WorkspaceEdit } from 'vscode';
1718
import { IApplicationShell, ICommandManager, IDocumentManager } from '../../client/common/application/types';
18-
import { Commands, EXTENSION_ROOT_DIR } from '../../client/common/constants';
19+
import { Commands, EXTENSION_ROOT_DIR, STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants';
1920
import { ProcessService } from '../../client/common/process/proc';
2021
import {
2122
IProcessServiceFactory,
@@ -27,10 +28,14 @@ import {
2728
IConfigurationService,
2829
IDisposableRegistry,
2930
IEditorUtils,
31+
IOutputChannel,
32+
IPersistentState,
33+
IPersistentStateFactory,
3034
IPythonSettings,
3135
ISortImportSettings
3236
} from '../../client/common/types';
3337
import { createDeferred, createDeferredFromPromise } from '../../client/common/utils/async';
38+
import { Common, Diagnostics } from '../../client/common/utils/localize';
3439
import { noop } from '../../client/common/utils/misc';
3540
import { IServiceContainer } from '../../client/ioc/types';
3641
import { SortImportsEditingProvider } from '../../client/providers/importSortProvider';
@@ -48,6 +53,8 @@ suite('Import Sort Provider', () => {
4853
let editorUtils: TypeMoq.IMock<IEditorUtils>;
4954
let commandManager: TypeMoq.IMock<ICommandManager>;
5055
let pythonSettings: TypeMoq.IMock<IPythonSettings>;
56+
let persistentStateFactory: TypeMoq.IMock<IPersistentStateFactory>;
57+
let output: TypeMoq.IMock<IOutputChannel>;
5158
let sortProvider: SortImportsEditingProvider;
5259
setup(() => {
5360
serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>();
@@ -59,6 +66,10 @@ suite('Import Sort Provider', () => {
5966
processServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>();
6067
pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>();
6168
editorUtils = TypeMoq.Mock.ofType<IEditorUtils>();
69+
persistentStateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>();
70+
output = TypeMoq.Mock.ofType<IOutputChannel>();
71+
serviceContainer.setup((c) => c.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL)).returns(() => output.object);
72+
serviceContainer.setup((c) => c.get(IPersistentStateFactory)).returns(() => persistentStateFactory.object);
6273
serviceContainer.setup((c) => c.get(ICommandManager)).returns(() => commandManager.object);
6374
serviceContainer.setup((c) => c.get(IDocumentManager)).returns(() => documentManager.object);
6475
serviceContainer.setup((c) => c.get(IApplicationShell)).returns(() => shell.object);
@@ -71,6 +82,10 @@ suite('Import Sort Provider', () => {
7182
sortProvider = new SortImportsEditingProvider(serviceContainer.object);
7283
});
7384

85+
teardown(() => {
86+
sinon.restore();
87+
});
88+
7489
test('Ensure command is registered', () => {
7590
commandManager
7691
.setup((c) =>
@@ -341,7 +356,7 @@ suite('Import Sort Provider', () => {
341356
p.execObservable(
342357
TypeMoq.It.isValue('CUSTOM_ISORT'),
343358
TypeMoq.It.isValue(expectedArgs),
344-
TypeMoq.It.isValue({ throwOnStdErr: true, token: undefined, cwd: path.sep })
359+
TypeMoq.It.isValue({ token: undefined, cwd: path.sep })
345360
)
346361
)
347362
.returns(() => executionResult)
@@ -428,7 +443,7 @@ suite('Import Sort Provider', () => {
428443
.setup((p) =>
429444
p.execObservable(
430445
TypeMoq.It.isValue(expectedArgs),
431-
TypeMoq.It.isValue({ throwOnStdErr: true, token: undefined, cwd: path.sep })
446+
TypeMoq.It.isValue({ token: undefined, cwd: path.sep })
432447
)
433448
)
434449
.returns(() => executionResult)
@@ -556,4 +571,126 @@ suite('Import Sort Provider', () => {
556571
expect(edit).to.be.equal(undefined, 'The results from the first execution should be discarded');
557572
stdinStream1.verifyAll();
558573
});
574+
575+
test('If isort raises a warning message related to isort5 upgrade guide, show message', async () => {
576+
const _showWarningAndOptionallyShowOutput = sinon.stub(
577+
SortImportsEditingProvider.prototype,
578+
'_showWarningAndOptionallyShowOutput'
579+
);
580+
_showWarningAndOptionallyShowOutput.resolves();
581+
const uri = Uri.file('something.py');
582+
const mockDoc = TypeMoq.Mock.ofType<TextDocument>();
583+
const processService = TypeMoq.Mock.ofType<ProcessService>();
584+
processService.setup((d: any) => d.then).returns(() => undefined);
585+
mockDoc.setup((d: any) => d.then).returns(() => undefined);
586+
mockDoc.setup((d) => d.lineCount).returns(() => 10);
587+
mockDoc.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => 'Hello');
588+
mockDoc.setup((d) => d.isDirty).returns(() => true);
589+
mockDoc.setup((d) => d.uri).returns(() => uri);
590+
documentManager
591+
.setup((d) => d.openTextDocument(TypeMoq.It.isValue(uri)))
592+
.returns(() => Promise.resolve(mockDoc.object));
593+
pythonSettings
594+
.setup((s) => s.sortImports)
595+
.returns(() => {
596+
return ({ path: 'CUSTOM_ISORT', args: [] } as any) as ISortImportSettings;
597+
});
598+
processServiceFactory
599+
.setup((p) => p.create(TypeMoq.It.isAny()))
600+
.returns(() => Promise.resolve(processService.object));
601+
const result = new WorkspaceEdit();
602+
editorUtils
603+
.setup((e) =>
604+
e.getWorkspaceEditsFromPatch(
605+
TypeMoq.It.isValue('Hello'),
606+
TypeMoq.It.isValue('DIFF'),
607+
TypeMoq.It.isAny()
608+
)
609+
)
610+
.returns(() => result);
611+
612+
// ----------------------Run the command----------------------
613+
let subscriber: Subscriber<Output<string>>;
614+
const stdinStream = TypeMoq.Mock.ofType<Writable>();
615+
stdinStream.setup((s) => s.write('Hello'));
616+
stdinStream
617+
.setup((s) => s.end())
618+
.callback(() => {
619+
subscriber.next({ source: 'stdout', out: 'DIFF' });
620+
subscriber.next({ source: 'stderr', out: 'Some warning related to isort5 (W0503)' });
621+
subscriber.complete();
622+
})
623+
.verifiable(TypeMoq.Times.once());
624+
const childProcess = TypeMoq.Mock.ofType<ChildProcess>();
625+
childProcess.setup((p) => p.stdin).returns(() => stdinStream.object);
626+
const executionResult = {
627+
proc: childProcess.object,
628+
out: new Observable<Output<string>>((s) => (subscriber = s)),
629+
dispose: noop
630+
};
631+
processService.reset();
632+
processService.setup((d: any) => d.then).returns(() => undefined);
633+
processService
634+
.setup((p) => p.execObservable(TypeMoq.It.isValue('CUSTOM_ISORT'), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
635+
.returns(() => executionResult);
636+
637+
const edit = await sortProvider.provideDocumentSortImportsEdits(uri);
638+
639+
// ----------------------Verify results----------------------
640+
expect(edit).to.be.equal(result, 'Execution result is incorrect');
641+
assert.ok(_showWarningAndOptionallyShowOutput.calledOnce);
642+
stdinStream.verifyAll();
643+
});
644+
645+
test('If user clicks show output on the isort5 warning prompt, show the Python output', async () => {
646+
const neverShowAgain = TypeMoq.Mock.ofType<IPersistentState<boolean>>();
647+
persistentStateFactory
648+
.setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAny(), false))
649+
.returns(() => neverShowAgain.object);
650+
neverShowAgain.setup((p) => p.value).returns(() => false);
651+
shell
652+
.setup((s) =>
653+
s.showWarningMessage(
654+
Diagnostics.checkIsort5UpgradeGuide(),
655+
Common.openOutputPanel(),
656+
Common.doNotShowAgain()
657+
)
658+
)
659+
.returns(() => Promise.resolve(Common.openOutputPanel()));
660+
output.setup((o) => o.show(true)).verifiable(TypeMoq.Times.once());
661+
await sortProvider._showWarningAndOptionallyShowOutput();
662+
output.verifyAll();
663+
});
664+
665+
test('If user clicks do not show again on the isort5 warning prompt, do not show the prompt again', async () => {
666+
const neverShowAgain = TypeMoq.Mock.ofType<IPersistentState<boolean>>();
667+
persistentStateFactory
668+
.setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAny(), false))
669+
.returns(() => neverShowAgain.object);
670+
let doNotShowAgainValue = false;
671+
neverShowAgain.setup((p) => p.value).returns(() => doNotShowAgainValue);
672+
neverShowAgain
673+
.setup((p) => p.updateValue(true))
674+
.returns(() => {
675+
doNotShowAgainValue = true;
676+
return Promise.resolve();
677+
});
678+
shell
679+
.setup((s) =>
680+
s.showWarningMessage(
681+
Diagnostics.checkIsort5UpgradeGuide(),
682+
Common.openOutputPanel(),
683+
Common.doNotShowAgain()
684+
)
685+
)
686+
.returns(() => Promise.resolve(Common.doNotShowAgain()))
687+
.verifiable(TypeMoq.Times.once());
688+
689+
await sortProvider._showWarningAndOptionallyShowOutput();
690+
shell.verifyAll();
691+
692+
await sortProvider._showWarningAndOptionallyShowOutput();
693+
await sortProvider._showWarningAndOptionallyShowOutput();
694+
shell.verifyAll();
695+
});
559696
});

0 commit comments

Comments
 (0)