Skip to content

Commit 3c552f9

Browse files
anthonykim1cwebster-99Kartik Raj
authored
Show warning and allow user to turn off smart send for deprecated Python code (microsoft#22353)
Resolves: microsoft#22341 microsoft#22340 Showing warning message after detecting user is on Python file with deprecated Python code, and are attempting to run smart send via shift+enter action. Allow user to turn off this via workspace setting. --------- Co-authored-by: Courtney Webster <[email protected]> Co-authored-by: Kartik Raj <[email protected]>
1 parent ef983f4 commit 3c552f9

File tree

14 files changed

+184
-17
lines changed

14 files changed

+184
-17
lines changed

package.json

+6
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,12 @@
704704
"scope": "resource",
705705
"type": "array"
706706
},
707+
"python.REPL.enableREPLSmartSend": {
708+
"default": true,
709+
"description": "%python.EnableREPLSmartSend.description%",
710+
"scope": "resource",
711+
"type": "boolean"
712+
},
707713
"python.testing.autoTestDiscoverOnSaveEnabled": {
708714
"default": true,
709715
"description": "%python.testing.autoTestDiscoverOnSaveEnabled.description%",

package.nls.json

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"python.missingPackage.severity.description": "Set severity of missing packages in requirements.txt or pyproject.toml",
6060
"python.pipenvPath.description": "Path to the pipenv executable to use for activation.",
6161
"python.poetryPath.description": "Path to the poetry executable.",
62+
"python.EnableREPLSmartSend.description": "Toggle Smart Send for the Python REPL. Smart Send enables sending the smallest runnable block of code to the REPL on Shift+Enter and moves the cursor accordingly.",
6263
"python.tensorBoard.logDirectory.description": "Set this setting to your preferred TensorBoard log directory to skip log directory prompt when starting TensorBoard.",
6364
"python.tensorBoard.logDirectory.markdownDeprecationMessage": "Tensorboard support has been moved to the extension [Tensorboard extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.tensorboard). Instead use the setting `tensorBoard.logDirectory`.",
6465
"python.tensorBoard.logDirectory.deprecationMessage": "Tensorboard support has been moved to the extension Tensorboard extension. Instead use the setting `tensorBoard.logDirectory`.",

pythonFiles/normalizeSelection.py

+22-5
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,18 @@ def traverse_file(wholeFileContent, start_line, end_line, was_highlighted):
150150
or a multiline dictionary, or differently styled multi-line list comprehension, etc.
151151
Then call the normalize_lines function to normalize our smartly selected code block.
152152
"""
153+
parsed_file_content = None
154+
155+
try:
156+
parsed_file_content = ast.parse(wholeFileContent)
157+
except Exception:
158+
# Handle case where user is attempting to run code where file contains deprecated Python code.
159+
# Let typescript side know and show warning message.
160+
return {
161+
"normalized_smart_result": "deprecated",
162+
"which_line_next": 0,
163+
}
153164

154-
parsed_file_content = ast.parse(wholeFileContent)
155165
smart_code = ""
156166
should_run_top_blocks = []
157167

@@ -267,7 +277,11 @@ def get_next_block_lineno(which_line_next):
267277
data = None
268278
which_line_next = 0
269279

270-
if empty_Highlight and contents.get("smartSendExperimentEnabled"):
280+
if (
281+
empty_Highlight
282+
and contents.get("smartSendExperimentEnabled")
283+
and contents.get("smartSendSettingsEnabled")
284+
):
271285
result = traverse_file(
272286
contents["wholeFileContent"],
273287
vscode_start_line,
@@ -276,9 +290,12 @@ def get_next_block_lineno(which_line_next):
276290
)
277291
normalized = result["normalized_smart_result"]
278292
which_line_next = result["which_line_next"]
279-
data = json.dumps(
280-
{"normalized": normalized, "nextBlockLineno": result["which_line_next"]}
281-
)
293+
if normalized == "deprecated":
294+
data = json.dumps({"normalized": normalized})
295+
else:
296+
data = json.dumps(
297+
{"normalized": normalized, "nextBlockLineno": result["which_line_next"]}
298+
)
282299
else:
283300
normalized = normalize_lines(contents["code"])
284301
data = json.dumps({"normalized": normalized})

src/client/common/application/commands.ts

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu
7272
];
7373
['workbench.action.files.openFolder']: [];
7474
['workbench.action.openWorkspace']: [];
75+
['workbench.action.openSettings']: [string];
7576
['setContext']: [string, boolean] | ['python.vscode.channel', Channel];
7677
['python.reloadVSCode']: [string];
7778
['revealLine']: [{ lineNumber: number; at: 'top' | 'center' | 'bottom' }];

src/client/common/configSettings.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
IInterpreterPathService,
2929
IInterpreterSettings,
3030
IPythonSettings,
31+
IREPLSettings,
3132
ITensorBoardSettings,
3233
ITerminalSettings,
3334
Resource,
@@ -111,6 +112,8 @@ export class PythonSettings implements IPythonSettings {
111112

112113
public globalModuleInstallation = false;
113114

115+
public REPL!: IREPLSettings;
116+
114117
public experiments!: IExperiments;
115118

116119
public languageServer: LanguageServerType = LanguageServerType.Node;
@@ -360,7 +363,8 @@ export class PythonSettings implements IPythonSettings {
360363
activateEnvInCurrentTerminal: false,
361364
};
362365

363-
const experiments = systemVariables.resolveAny(pythonSettings.get<IExperiments>('experiments'))!;
366+
this.REPL = pythonSettings.get<IREPLSettings>('REPL')!;
367+
const experiments = pythonSettings.get<IExperiments>('experiments')!;
364368
if (this.experiments) {
365369
Object.assign<IExperiments, IExperiments>(this.experiments, experiments);
366370
} else {

src/client/common/types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ export interface IPythonSettings {
178178
readonly languageServerIsDefault: boolean;
179179
readonly defaultInterpreterPath: string;
180180
readonly tensorBoard: ITensorBoardSettings | undefined;
181+
readonly REPL: IREPLSettings;
181182
register(): void;
182183
}
183184

@@ -197,6 +198,10 @@ export interface ITerminalSettings {
197198
readonly activateEnvInCurrentTerminal: boolean;
198199
}
199200

201+
export interface IREPLSettings {
202+
readonly enableREPLSmartSend: boolean;
203+
}
204+
200205
export interface IExperiments {
201206
/**
202207
* Return `true` if experiments are enabled, else `false`.

src/client/common/utils/localize.ts

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
'use strict';
55

66
import { l10n } from 'vscode';
7+
import { Commands } from '../constants';
78

89
/* eslint-disable @typescript-eslint/no-namespace, no-shadow */
910

@@ -42,6 +43,9 @@ export namespace Diagnostics {
4243
export const pylanceDefaultMessage = l10n.t(
4344
"The Python extension now includes Pylance to improve completions, code navigation, overall performance and much more! You can learn more about the update and learn how to change your language server [here](https://aka.ms/new-python-bundle).\n\nRead Pylance's license [here](https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license).",
4445
);
46+
export const invalidSmartSendMessage = l10n.t(`Python is unable to parse the code provided. Please
47+
turn off Smart Send if you wish to always run line by line or explicitly select code
48+
to force run. See [logs](command:${Commands.ViewOutput}) for more details`);
4549
}
4650

4751
export namespace Common {
@@ -92,6 +96,9 @@ export namespace AttachProcess {
9296
export const refreshList = l10n.t('Refresh process list');
9397
}
9498

99+
export namespace Repl {
100+
export const disableSmartSend = l10n.t('Disable Smart Send');
101+
}
95102
export namespace Pylance {
96103
export const remindMeLater = l10n.t('Remind me later');
97104

src/client/terminals/codeExecution/djangoShellCodeExecution.ts

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvi
3636
disposableRegistry,
3737
platformService,
3838
interpreterService,
39+
commandManager,
3940
);
4041
this.terminalTitle = 'Django Shell';
4142
disposableRegistry.push(new DjangoContextInitializer(documentManager, workspace, fileSystem, commandManager));

src/client/terminals/codeExecution/helper.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { inject, injectable } from 'inversify';
66
import { l10n, Position, Range, TextEditor, Uri } from 'vscode';
77

88
import {
9+
IActiveResourceService,
910
IApplicationShell,
1011
ICommandManager,
1112
IDocumentManager,
@@ -36,6 +37,8 @@ export class CodeExecutionHelper implements ICodeExecutionHelper {
3637

3738
private readonly commandManager: ICommandManager;
3839

40+
private activeResourceService: IActiveResourceService;
41+
3942
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
4043
// @ts-expect-error TS6133: 'configSettings' is declared but its value is never read.
4144
private readonly configSettings: IConfigurationService;
@@ -47,6 +50,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper {
4750
this.interpreterService = serviceContainer.get<IInterpreterService>(IInterpreterService);
4851
this.configSettings = serviceContainer.get<IConfigurationService>(IConfigurationService);
4952
this.commandManager = serviceContainer.get<ICommandManager>(ICommandManager);
53+
this.activeResourceService = this.serviceContainer.get<IActiveResourceService>(IActiveResourceService);
5054
}
5155

5256
public async normalizeLines(code: string, wholeFileContent?: string, resource?: Uri): Promise<string> {
@@ -90,13 +94,21 @@ export class CodeExecutionHelper implements ICodeExecutionHelper {
9094
const endLineVal = activeEditor?.selection?.end.line ?? 0;
9195
const emptyHighlightVal = activeEditor?.selection?.isEmpty ?? true;
9296
const smartSendExperimentEnabledVal = pythonSmartSendEnabled(this.serviceContainer);
97+
let smartSendSettingsEnabledVal = false;
98+
const configuration = this.serviceContainer.get<IConfigurationService>(IConfigurationService);
99+
if (configuration) {
100+
const pythonSettings = configuration.getSettings(this.activeResourceService.getActiveResource());
101+
smartSendSettingsEnabledVal = pythonSettings.REPL.enableREPLSmartSend;
102+
}
103+
93104
const input = JSON.stringify({
94105
code,
95106
wholeFileContent,
96107
startLine: startLineVal,
97108
endLine: endLineVal,
98109
emptyHighlight: emptyHighlightVal,
99110
smartSendExperimentEnabled: smartSendExperimentEnabledVal,
111+
smartSendSettingsEnabled: smartSendSettingsEnabledVal,
100112
});
101113
observable.proc?.stdin?.write(input);
102114
observable.proc?.stdin?.end();
@@ -105,7 +117,12 @@ export class CodeExecutionHelper implements ICodeExecutionHelper {
105117
const result = await normalizeOutput.promise;
106118
const object = JSON.parse(result);
107119

108-
if (activeEditor?.selection) {
120+
if (
121+
activeEditor?.selection &&
122+
smartSendExperimentEnabledVal &&
123+
smartSendSettingsEnabledVal &&
124+
object.normalized !== 'deprecated'
125+
) {
109126
const lineOffset = object.nextBlockLineno - activeEditor!.selection.start.line - 1;
110127
await this.moveToNextBlock(lineOffset, activeEditor);
111128
}

src/client/terminals/codeExecution/repl.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { inject, injectable } from 'inversify';
77
import { Disposable } from 'vscode';
8-
import { IWorkspaceService } from '../../common/application/types';
8+
import { ICommandManager, IWorkspaceService } from '../../common/application/types';
99
import { IPlatformService } from '../../common/platform/types';
1010
import { ITerminalServiceFactory } from '../../common/terminal/types';
1111
import { IConfigurationService, IDisposableRegistry } from '../../common/types';
@@ -21,6 +21,7 @@ export class ReplProvider extends TerminalCodeExecutionProvider {
2121
@inject(IDisposableRegistry) disposableRegistry: Disposable[],
2222
@inject(IPlatformService) platformService: IPlatformService,
2323
@inject(IInterpreterService) interpreterService: IInterpreterService,
24+
@inject(ICommandManager) commandManager: ICommandManager,
2425
) {
2526
super(
2627
terminalServiceFactory,
@@ -29,6 +30,7 @@ export class ReplProvider extends TerminalCodeExecutionProvider {
2930
disposableRegistry,
3031
platformService,
3132
interpreterService,
33+
commandManager,
3234
);
3335
this.terminalTitle = 'REPL';
3436
}

src/client/terminals/codeExecution/terminalCodeExecution.ts

+15-4
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@
66
import { inject, injectable } from 'inversify';
77
import * as path from 'path';
88
import { Disposable, Uri } from 'vscode';
9-
import { IWorkspaceService } from '../../common/application/types';
9+
import { ICommandManager, IWorkspaceService } from '../../common/application/types';
1010
import '../../common/extensions';
1111
import { IPlatformService } from '../../common/platform/types';
1212
import { ITerminalService, ITerminalServiceFactory } from '../../common/terminal/types';
1313
import { IConfigurationService, IDisposableRegistry, Resource } from '../../common/types';
14+
import { Diagnostics, Repl } from '../../common/utils/localize';
15+
import { showWarningMessage } from '../../common/vscodeApis/windowApis';
1416
import { IInterpreterService } from '../../interpreter/contracts';
17+
import { traceInfo } from '../../logging';
1518
import { buildPythonExecInfo, PythonExecInfo } from '../../pythonEnvironments/exec';
1619
import { ICodeExecutionService } from '../../terminals/types';
17-
1820
@injectable()
1921
export class TerminalCodeExecutionProvider implements ICodeExecutionService {
2022
private hasRanOutsideCurrentDrive = false;
@@ -27,6 +29,7 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService {
2729
@inject(IDisposableRegistry) protected readonly disposables: Disposable[],
2830
@inject(IPlatformService) protected readonly platformService: IPlatformService,
2931
@inject(IInterpreterService) protected readonly interpreterService: IInterpreterService,
32+
@inject(ICommandManager) protected readonly commandManager: ICommandManager,
3033
) {}
3134

3235
public async executeFile(file: Uri, options?: { newTerminalPerFile: boolean }) {
@@ -42,9 +45,17 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService {
4245
if (!code || code.trim().length === 0) {
4346
return;
4447
}
45-
4648
await this.initializeRepl(resource);
47-
await this.getTerminalService(resource).sendText(code);
49+
if (code == 'deprecated') {
50+
// If user is trying to smart send deprecated code show warning
51+
const selection = await showWarningMessage(Diagnostics.invalidSmartSendMessage, Repl.disableSmartSend);
52+
traceInfo(`Selected file contains invalid Python or Deprecated Python 2 code`);
53+
if (selection === Repl.disableSmartSend) {
54+
this.configurationService.updateSetting('REPL.enableREPLSmartSend', false, resource);
55+
}
56+
} else {
57+
await this.getTerminalService(resource).sendText(code);
58+
}
4859
}
4960
public async initializeRepl(resource: Resource) {
5061
const terminalService = this.getTerminalService(resource);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
def beliebig(x, y, *mehr):
2+
print "x=", x, ", x=", y
3+
print "mehr: ", mehr
4+
5+
list = [
6+
1,
7+
2,
8+
3,
9+
]
10+
print("Above is invalid");print("deprecated");print("show warning")

0 commit comments

Comments
 (0)