Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancements to running code in a terminal #1432

Merged
merged 12 commits into from
Apr 23, 2018
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/1 Enhancements/1207.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Remove empty spaces from the selected text of the active editor when executing in a terminal.
1 change: 1 addition & 0 deletions news/1 Enhancements/1316.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Save the python file before running it in the terminal using the command/menu `Run Python File in Terminal`.
1 change: 1 addition & 0 deletions news/1 Enhancements/1349.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `Ctrl+Enter` keyboard shortcut for `Run Selection/Line in Python Terminal`.
1 change: 1 addition & 0 deletions news/2 Fixes/259.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add blank lines to seprate blocks of indented code (function defs, classes, and the like) to ensure the code can be run within a Python interactive prompt.
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@
"path": "./snippets/python.json"
}
],
"keybindings":[
{
"command": "python.execSelectionInTerminal",
"key": "ctrl+enter"
}
],
"commands": [
{
"command": "python.sortImports",
Expand Down
1 change: 1 addition & 0 deletions pythonFiles/experimental/ptvsd
Submodule ptvsd added at 835578
59 changes: 59 additions & 0 deletions pythonFiles/normalizeForInterpreter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import io
import os
import sys
import token
import tokenize

try:
unicode
except:
unicode = str


def normalizeLines(content):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalize_lines

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aaarg..

"""Removes empty lines and adds empty lines only to sepaparate
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstrings always have a single line summary that fits within the 80 column limit, then a newline, then anything extra. E.g.

def normalize_lines(content):
    """Normalize blank lines for sending to the terminal.

    Blank lines within a statement block are removed to prevent the REPL
    from thinking the block is finished. Newlines are added to separate
    top-level statements so that the REPL does not think there is a syntax
    error.

    """
    lines = content.splitlines(False)
    # ...

indented code. So that the code can be used for execution in
the Python interactive prompt """

lines = content.splitlines(False)

# Find out if we have any trailing blank lines
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a period.

has_blank_lines = len(lines[-1].strip()) == 0 or content.endswith(os.linesep)

# Remove empty lines
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a period.

tokens = tokenize.generate_tokens(io.StringIO(content).readline)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI that is undocumented in Python 3. Is there a reason you aren't using tokenize.tokenize()?


new_lines_to_remove = []
for toknum, _, spos, epos, line in tokens:
if token.tok_name[toknum] == 'NL' and len(line.strip()) == 0 and spos[0] == epos[0]:
new_lines_to_remove.append(spos[0] - 1)

for line_index in reversed(new_lines_to_remove):
lines.pop(line_index)

# Add new lines just before every dedent
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a period.

content = os.linesep.join(lines)
tokens = tokenize.generate_tokens(io.StringIO(content).readline)
dedented_lines = []
for toknum, _, spos, epos, line in tokens:
if toknum == token.DEDENT and spos[0] == epos[0] and spos[0] <= len(lines):
index = spos[0] - 1
if not index in dedented_lines:
dedented_lines.append(index)

for line_index in reversed(dedented_lines):
line = lines[line_index]
indent_size = line.index(line.strip())
indentation = line[0:indent_size]
lines.insert(line_index, indentation)

sys.stdout.write(os.linesep.join(lines) + (os.linesep if has_blank_lines else ''))
sys.stdout.flush()


if __name__ == '__main__':
contents = unicode(sys.argv[1])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are you trying to accomplish here? This is a rather dramatic decoding if you were handed bytes through sys.argv.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When sending via stdin for python2, bytes get sent in stdin.

normalizeLines(contents)
2 changes: 1 addition & 1 deletion src/client/common/configSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings {
public formatting?: IFormattingSettings;
public autoComplete?: IAutoCompleteSettings;
public unitTest?: IUnitTestSettings;
public terminal?: ITerminalSettings;
public terminal!: ITerminalSettings;
public sortImports?: ISortImportSettings;
public workspaceSymbols?: IWorkspaceSymbolSettings;
public disableInstallationChecks = false;
Expand Down
2 changes: 1 addition & 1 deletion src/client/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export interface IPythonSettings {
readonly formatting?: IFormattingSettings;
readonly unitTest?: IUnitTestSettings;
readonly autoComplete?: IAutoCompleteSettings;
readonly terminal?: ITerminalSettings;
readonly terminal: ITerminalSettings;
readonly sortImports?: ISortImportSettings;
readonly workspaceSymbols?: IWorkspaceSymbolSettings;
readonly envFile: string;
Expand Down
5 changes: 3 additions & 2 deletions src/client/terminals/codeExecution/codeExecutionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export class CodeExecutionManager implements ICodeExecutionManager {
if (!fileToExecute) {
return;
}
await codeExecutionHelper.saveFileIfDirty(fileToExecute);
const executionService = this.serviceContainer.get<ICodeExecutionService>(ICodeExecutionService, 'standard');
await executionService.executeFile(fileToExecute);
}
Expand All @@ -59,11 +60,11 @@ export class CodeExecutionManager implements ICodeExecutionManager {
}
const codeExecutionHelper = this.serviceContainer.get<ICodeExecutionHelper>(ICodeExecutionHelper);
const codeToExecute = await codeExecutionHelper.getSelectedTextToExecute(activeEditor!);
const normalizedCode = codeExecutionHelper.normalizeLines(codeToExecute!);
const normalizedCode = await codeExecutionHelper.normalizeLines(codeToExecute!);
if (!normalizedCode || normalizedCode.trim().length === 0) {
return;
}

await executionService.execute(codeToExecute!, activeEditor!.document.uri);
await executionService.execute(normalizedCode, activeEditor!.document.uri);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvi
public getReplCommandArgs(resource?: Uri): { command: string; args: string[] } {
const pythonSettings = this.configurationService.getSettings(resource);
const command = this.platformService.isWindows ? pythonSettings.pythonPath.replace(/\\/g, '/') : pythonSettings.pythonPath;
const args = pythonSettings.terminal!.launchArgs.slice();
const args = pythonSettings.terminal.launchArgs.slice();

const workspaceUri = resource ? this.workspace.getWorkspaceFolder(resource) : undefined;
const defaultWorkspace = Array.isArray(this.workspace.workspaceFolders) && this.workspace.workspaceFolders.length > 0 ? this.workspace.workspaceFolders[0].uri.fsPath : '';
Expand Down
49 changes: 40 additions & 9 deletions src/client/terminals/codeExecution/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,45 @@
// Licensed under the MIT License.

import { inject, injectable } from 'inversify';
import { EOL } from 'os';
import * as path from 'path';
import { Range, TextEditor, Uri } from 'vscode';
import { IApplicationShell, IDocumentManager } from '../../common/application/types';
import { PythonLanguage } from '../../common/constants';
import { EXTENSION_ROOT_DIR, PythonLanguage } from '../../common/constants';
import '../../common/extensions';
import { IProcessService } from '../../common/process/types';
import { IConfigurationService } from '../../common/types';
import { IEnvironmentVariablesProvider } from '../../common/variables/types';
import { IServiceContainer } from '../../ioc/types';
import { ICodeExecutionHelper } from '../types';

@injectable()
export class CodeExecutionHelper implements ICodeExecutionHelper {
constructor( @inject(IDocumentManager) private documentManager: IDocumentManager,
@inject(IApplicationShell) private applicationShell: IApplicationShell) {

private readonly documentManager: IDocumentManager;
private readonly applicationShell: IApplicationShell;
private readonly envVariablesProvider: IEnvironmentVariablesProvider;
private readonly processService: IProcessService;
private readonly configurationService: IConfigurationService;
constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
this.documentManager = serviceContainer.get<IDocumentManager>(IDocumentManager);
this.applicationShell = serviceContainer.get<IApplicationShell>(IApplicationShell);
this.envVariablesProvider = serviceContainer.get<IEnvironmentVariablesProvider>(IEnvironmentVariablesProvider);
this.processService = serviceContainer.get<IProcessService>(IProcessService);
this.configurationService = serviceContainer.get<IConfigurationService>(IConfigurationService);
}
public normalizeLines(code: string): string {
const codeLines = code.splitLines({ trim: false, removeEmptyEntries: false });
const codeLinesWithoutEmptyLines = codeLines.filter(line => line.trim().length > 0);
return codeLinesWithoutEmptyLines.join(EOL);
public async normalizeLines(code: string, resource?: Uri): Promise<string> {
try {
if (code.trim().length === 0) {
return '';
}
const env = await this.envVariablesProvider.getEnvironmentVariables(resource);
const pythonPath = this.configurationService.getSettings(resource).pythonPath;
const args = [path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'normalizeForInterpreter.py'), code];
const proc = await this.processService.exec(pythonPath, args, { env, throwOnStdErr: true });
return proc.stdout;
} catch (ex) {
console.error(ex, 'Python: Failed to normalize code for execution in terminal');
return code;
}
}

public async getFileToExecute(): Promise<Uri | undefined> {
Expand All @@ -35,6 +57,9 @@ export class CodeExecutionHelper implements ICodeExecutionHelper {
this.applicationShell.showErrorMessage('The active file is not a Python source file');
return;
}
if (activeEditor.document.isDirty) {
await activeEditor.document.save();
}
return activeEditor.document.uri;
}

Expand All @@ -53,4 +78,10 @@ export class CodeExecutionHelper implements ICodeExecutionHelper {
}
return code;
}
public async saveFileIfDirty(file: Uri): Promise<void> {
const docs = this.documentManager.textDocuments.filter(d => d.uri.path === file.path);
if (docs.length === 1 && docs[0].isDirty) {
await docs[0].save();
}
}
}
11 changes: 5 additions & 6 deletions src/client/terminals/codeExecution/terminalCodeExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,15 @@ import { IWorkspaceService } from '../../common/application/types';
import '../../common/extensions';
import { IPlatformService } from '../../common/platform/types';
import { ITerminalService, ITerminalServiceFactory } from '../../common/terminal/types';
import { IConfigurationService } from '../../common/types';
import { IDisposableRegistry } from '../../common/types';
import { IConfigurationService, IDisposableRegistry } from '../../common/types';
import { ICodeExecutionService } from '../../terminals/types';

@injectable()
export class TerminalCodeExecutionProvider implements ICodeExecutionService {
protected terminalTitle: string;
private _terminalService: ITerminalService;
protected terminalTitle!: string;
private _terminalService!: ITerminalService;
private replActive?: Promise<boolean>;
constructor( @inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory,
constructor(@inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory,
@inject(IConfigurationService) protected readonly configurationService: IConfigurationService,
@inject(IWorkspaceService) protected readonly workspace: IWorkspaceService,
@inject(IDisposableRegistry) protected readonly disposables: Disposable[],
Expand Down Expand Up @@ -60,7 +59,7 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService {

await this.replActive;
}
public getReplCommandArgs(resource?: Uri): { command: string, args: string[] } {
public getReplCommandArgs(resource?: Uri): { command: string; args: string[] } {
const pythonSettings = this.configurationService.getSettings(resource);
const command = this.platformService.isWindows ? pythonSettings.pythonPath.replace(/\\/g, '/') : pythonSettings.pythonPath;
const args = pythonSettings.terminal.launchArgs.slice();
Expand Down
3 changes: 2 additions & 1 deletion src/client/terminals/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ export interface ICodeExecutionService {
export const ICodeExecutionHelper = Symbol('ICodeExecutionHelper');

export interface ICodeExecutionHelper {
normalizeLines(code: string): string;
normalizeLines(code: string): Promise<string>;
getFileToExecute(): Promise<Uri | undefined>;
saveFileIfDirty(file: Uri): Promise<void>;
getSelectedTextToExecute(textEditor: TextEditor): Promise<string | undefined>;
}

Expand Down
2 changes: 1 addition & 1 deletion src/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ process.env.IS_MULTI_ROOT_TEST = IS_MULTI_ROOT_TEST.toString();
// If running on CI server and we're running the debugger tests, then ensure we only run debug tests.
// We do this to ensure we only run debugger test, as debugger tests are very flaky on CI.
// So the solution is to run them separately and first on CI.
const grep = IS_CI_SERVER && IS_CI_SERVER_TEST_DEBUGGER ? 'Debug' : undefined;
const grep = IS_CI_SERVER && IS_CI_SERVER_TEST_DEBUGGER ? 'Debug' : 'Terminal - Code Execution Helper';

// You can directly control Mocha options by uncommenting the following lines.
// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info.
Expand Down
19 changes: 19 additions & 0 deletions src/test/pythonFiles/terminalExec/sample1_normalized.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Sample block 1
def square(x):
return x**2

print('hello')
# Sample block 2
a = 2
if a < 2:
print('less than 2')

else:
print('more than 2')

print('hello')
# Sample block 3
for i in range(5):
print(i)

print('complete')
18 changes: 18 additions & 0 deletions src/test/pythonFiles/terminalExec/sample1_raw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Sample block 1
def square(x):
return x**2

print('hello')
# Sample block 2
a = 2
if a < 2:
print('less than 2')
else:
print('more than 2')

print('hello')
# Sample block 3
for i in range(5):
print(i)

print('complete')
7 changes: 7 additions & 0 deletions src/test/pythonFiles/terminalExec/sample2_normalized.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
def add(x, y):
"""Adds x to y"""
# Some comment
return x + y

v = add(1, 7)
print(v)
8 changes: 8 additions & 0 deletions src/test/pythonFiles/terminalExec/sample2_raw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
def add(x, y):
"""Adds x to y"""
# Some comment

return x + y

v = add(1, 7)
print(v)
5 changes: 5 additions & 0 deletions src/test/pythonFiles/terminalExec/sample3_normalized.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
if True:
print(1)
print(2)

print(3)
5 changes: 5 additions & 0 deletions src/test/pythonFiles/terminalExec/sample3_raw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
if True:
print(1)

print(2)
print(3)
7 changes: 7 additions & 0 deletions src/test/pythonFiles/terminalExec/sample4_normalized.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class pc(object):
def __init__(self, pcname, model):
self.pcname = pcname
self.model = model

def print_name(self):
print('Workstation name is', self.pcname, 'model is', self.model)
7 changes: 7 additions & 0 deletions src/test/pythonFiles/terminalExec/sample4_raw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class pc(object):
def __init__(self, pcname, model):
self.pcname = pcname
self.model = model

def print_name(self):
print('Workstation name is', self.pcname, 'model is', self.model)
9 changes: 9 additions & 0 deletions src/test/pythonFiles/terminalExec/sample5_normalized.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
for i in range(10):
print('a')
for j in range(5):
print('b')
print('b2')
for k in range(2):
print('c')

print('done with first loop')
11 changes: 11 additions & 0 deletions src/test/pythonFiles/terminalExec/sample5_raw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
for i in range(10):
print('a')
for j in range(5):
print('b')

print('b2')

for k in range(2):
print('c')

print('done with first loop')
Loading