Skip to content

Commit f82af7a

Browse files
authored
Complete refactoring of code to use new code exec engine (microsoft#493)
Fixes microsoft#491
1 parent b1a3992 commit f82af7a

27 files changed

+279
-267
lines changed

src/client/common/process/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ export type ExecutionResult<T extends string | Buffer> = {
3737
export const IProcessService = Symbol('IProcessService');
3838

3939
export interface IProcessService {
40-
execObservable(file: string, args: string[], options: SpawnOptions): ObservableExecutionResult<string>;
41-
exec(file: string, args: string[], options: SpawnOptions): Promise<ExecutionResult<string>>;
40+
execObservable(file: string, args: string[], options?: SpawnOptions): ObservableExecutionResult<string>;
41+
exec(file: string, args: string[], options?: SpawnOptions): Promise<ExecutionResult<string>>;
4242
}
4343

4444
export const IPythonExecutionFactory = Symbol('IPythonExecutionFactory');

src/client/common/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ export enum Product {
5858
mypy = 11,
5959
unittest = 12,
6060
ctags = 13,
61-
rope = 14
61+
rope = 14,
62+
isort = 15
6263
}
6364

6465
export enum ModuleNamePurpose {

src/client/common/utils.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use strict';
22
// tslint:disable: no-any one-line no-suspicious-comment prefer-template prefer-const no-unnecessary-callback-wrapper no-function-expression no-string-literal no-control-regex no-shadowed-variable
33

4-
import * as child_process from 'child_process';
54
import * as fs from 'fs';
65
import * as os from 'os';
76
import * as path from 'path';
@@ -30,18 +29,6 @@ export function fsReaddirAsync(root: string): Promise<string[]> {
3029
});
3130
}
3231

33-
export async function getPathFromPythonCommand(pythonPath: string): Promise<string> {
34-
return await new Promise<string>((resolve, reject) => {
35-
child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => {
36-
if (stdout) {
37-
const lines = stdout.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0);
38-
resolve(lines.length > 0 ? lines[0] : '');
39-
} else {
40-
reject();
41-
}
42-
});
43-
});
44-
}
4532
export function formatErrorForLogging(error: Error | string): string {
4633
let message: string = '';
4734
if (typeof error === 'string') {

src/client/extension.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { STANDARD_OUTPUT_CHANNEL } from './common/constants';
1414
import { FeatureDeprecationManager } from './common/featureDeprecationManager';
1515
import { createDeferred } from './common/helpers';
1616
import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry';
17+
import { IProcessService, IPythonExecutionFactory } from './common/process/types';
1718
import { registerTypes as commonRegisterTypes } from './common/serviceRegistry';
1819
import { GLOBAL_MEMENTO, IDisposableRegistry, ILogger, IMemento, IOutputChannel, IPersistentStateFactory, WORKSPACE_MEMENTO } from './common/types';
1920
import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry';
@@ -85,23 +86,24 @@ export async function activate(context: vscode.ExtensionContext) {
8586
const pythonSettings = settings.PythonSettings.getInstance();
8687
sendStartupTelemetry(activated, serviceContainer);
8788

88-
sortImports.activate(context, standardOutputChannel);
89+
sortImports.activate(context, standardOutputChannel, serviceContainer);
8990
const interpreterManager = new InterpreterManager(serviceContainer);
9091
// This must be completed before we can continue.
9192
await interpreterManager.autoSetInterpreter();
9293

9394
interpreterManager.refresh()
9495
.catch(ex => console.error('Python Extension: interpreterManager.refresh', ex));
9596
context.subscriptions.push(interpreterManager);
97+
const processService = serviceContainer.get<IProcessService>(IProcessService);
9698
const interpreterVersionService = serviceContainer.get<IInterpreterVersionService>(IInterpreterVersionService);
97-
context.subscriptions.push(new SetInterpreterProvider(interpreterManager, interpreterVersionService));
99+
context.subscriptions.push(new SetInterpreterProvider(interpreterManager, interpreterVersionService, processService));
98100
context.subscriptions.push(...activateExecInTerminalProvider());
99101
context.subscriptions.push(activateUpdateSparkLibraryProvider());
100102
activateSimplePythonRefactorProvider(context, standardOutputChannel, serviceContainer);
101103
const jediFactory = new JediFactory(context.asAbsolutePath('.'), serviceContainer);
102104
context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory));
103105

104-
context.subscriptions.push(new ReplProvider());
106+
context.subscriptions.push(new ReplProvider(serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory)));
105107

106108
// Enable indentAction
107109
// tslint:disable-next-line:no-non-null-assertion
@@ -130,7 +132,7 @@ export async function activate(context: vscode.ExtensionContext) {
130132
context.subscriptions.push(vscode.languages.registerHoverProvider(PYTHON, new PythonHoverProvider(jediFactory)));
131133
context.subscriptions.push(vscode.languages.registerReferenceProvider(PYTHON, new PythonReferenceProvider(jediFactory)));
132134
context.subscriptions.push(vscode.languages.registerCompletionItemProvider(PYTHON, new PythonCompletionItemProvider(jediFactory), '.'));
133-
context.subscriptions.push(vscode.languages.registerCodeLensProvider(PYTHON, new ShebangCodeLensProvider()));
135+
context.subscriptions.push(vscode.languages.registerCodeLensProvider(PYTHON, new ShebangCodeLensProvider(processService)));
134136

135137
const symbolProvider = new PythonSymbolProvider(jediFactory);
136138
context.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(PYTHON, symbolProvider));

src/client/interpreter/configuration/setInterpreterProvider.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as path from 'path';
22
import { commands, ConfigurationTarget, Disposable, QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode';
33
import { InterpreterManager } from '../';
44
import * as settings from '../../common/configSettings';
5+
import { IProcessService } from '../../common/process/types';
56
import { IInterpreterVersionService, PythonInterpreter, WorkspacePythonPath } from '../contracts';
67
import { ShebangCodeLensProvider } from '../display/shebangCodeLensProvider';
78
import { PythonPathUpdaterService } from './pythonPathUpdaterService';
@@ -15,7 +16,9 @@ interface PythonPathQuickPickItem extends QuickPickItem {
1516
export class SetInterpreterProvider implements Disposable {
1617
private disposables: Disposable[] = [];
1718
private pythonPathUpdaterService: PythonPathUpdaterService;
18-
constructor(private interpreterManager: InterpreterManager, interpreterVersionService: IInterpreterVersionService) {
19+
constructor(private interpreterManager: InterpreterManager,
20+
interpreterVersionService: IInterpreterVersionService,
21+
private processService: IProcessService) {
1922
this.disposables.push(commands.registerCommand('python.setInterpreter', this.setInterpreter.bind(this)));
2023
this.disposables.push(commands.registerCommand('python.setShebangInterpreter', this.setShebangInterpreter.bind(this)));
2124
this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), interpreterVersionService);
@@ -88,7 +91,7 @@ export class SetInterpreterProvider implements Disposable {
8891
}
8992

9093
private async setShebangInterpreter(): Promise<void> {
91-
const shebang = await ShebangCodeLensProvider.detectShebang(window.activeTextEditor!.document);
94+
const shebang = await new ShebangCodeLensProvider(this.processService).detectShebang(window.activeTextEditor!.document);
9295
if (!shebang) {
9396
return;
9497
}

src/client/interpreter/display/index.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1-
import * as child_process from 'child_process';
21
import { EOL } from 'os';
32
import * as path from 'path';
43
import { Disposable, StatusBarItem, Uri } from 'vscode';
54
import { PythonSettings } from '../../common/configSettings';
5+
import { IProcessService } from '../../common/process/types';
66
import * as utils from '../../common/utils';
77
import { IInterpreterLocatorService, IInterpreterVersionService } from '../contracts';
8-
import { getActiveWorkspaceUri, getFirstNonEmptyLineFromMultilineString } from '../helpers';
8+
import { getActiveWorkspaceUri } from '../helpers';
99
import { IVirtualEnvironmentManager } from '../virtualEnvs/types';
1010

1111
// tslint:disable-next-line:completed-docs
1212
export class InterpreterDisplay implements Disposable {
1313
constructor(private statusBar: StatusBarItem,
1414
private interpreterLocator: IInterpreterLocatorService,
1515
private virtualEnvMgr: IVirtualEnvironmentManager,
16-
private versionProvider: IInterpreterVersionService) {
16+
private versionProvider: IInterpreterVersionService,
17+
private processService: IProcessService) {
1718

1819
this.statusBar.command = 'python.setInterpreter';
1920
}
@@ -69,11 +70,8 @@ export class InterpreterDisplay implements Disposable {
6970
.then(env => env ? env.name : '');
7071
}
7172
private async getFullyQualifiedPathToInterpreter(pythonPath: string) {
72-
return new Promise<string>(resolve => {
73-
child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => {
74-
resolve(getFirstNonEmptyLineFromMultilineString(stdout));
75-
});
76-
})
73+
return this.processService.exec(pythonPath, ['-c', 'import sys;print(sys.executable)'])
74+
.then(output => output.stdout.trim())
7775
.then(value => value.length === 0 ? pythonPath : value)
7876
.catch(() => pythonPath);
7977
}

src/client/interpreter/display/shebangCodeLensProvider.ts

Lines changed: 20 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1-
'use strict';
2-
import * as child_process from 'child_process';
31
import * as vscode from 'vscode';
42
import { CancellationToken, CodeLens, TextDocument } from 'vscode';
53
import * as settings from '../../common/configSettings';
4+
import { IProcessService } from '../../common/process/types';
65
import { IS_WINDOWS } from '../../common/utils';
7-
import { getFirstNonEmptyLineFromMultilineString } from '../../interpreter/helpers';
86

97
export class ShebangCodeLensProvider implements vscode.CodeLensProvider {
10-
// tslint:disable-next-line:prefer-type-cast no-any
8+
// tslint:disable-next-line:no-any
119
public onDidChangeCodeLenses: vscode.Event<void> = vscode.workspace.onDidChangeConfiguration as any as vscode.Event<void>;
12-
// tslint:disable-next-line:function-name
13-
public static async detectShebang(document: TextDocument): Promise<string | undefined> {
10+
constructor(private processService: IProcessService) { }
11+
public async detectShebang(document: TextDocument): Promise<string | undefined> {
1412
const firstLine = document.lineAt(0);
1513
if (firstLine.isEmptyOrWhitespace) {
1614
return;
@@ -21,40 +19,30 @@ export class ShebangCodeLensProvider implements vscode.CodeLensProvider {
2119
}
2220

2321
const shebang = firstLine.text.substr(2).trim();
24-
const pythonPath = await ShebangCodeLensProvider.getFullyQualifiedPathToInterpreter(shebang);
22+
const pythonPath = await this.getFullyQualifiedPathToInterpreter(shebang);
2523
return typeof pythonPath === 'string' && pythonPath.length > 0 ? pythonPath : undefined;
2624
}
27-
private static async getFullyQualifiedPathToInterpreter(pythonPath: string) {
28-
if (pythonPath.indexOf('bin/env ') >= 0 && !IS_WINDOWS) {
29-
// In case we have pythonPath as '/usr/bin/env python'
30-
return new Promise<string>(resolve => {
31-
const command = child_process.exec(`${pythonPath} -c 'import sys;print(sys.executable)'`);
32-
let result = '';
33-
command.stdout.on('data', (data) => {
34-
result += data.toString();
35-
});
36-
command.on('close', () => {
37-
resolve(getFirstNonEmptyLineFromMultilineString(result));
38-
});
39-
});
40-
} else {
41-
return new Promise<string>(resolve => {
42-
child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => {
43-
resolve(getFirstNonEmptyLineFromMultilineString(stdout));
44-
});
45-
});
46-
}
47-
}
48-
4925
public async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise<CodeLens[]> {
5026
const codeLenses = await this.createShebangCodeLens(document);
5127
return Promise.resolve(codeLenses);
5228
}
53-
29+
private async getFullyQualifiedPathToInterpreter(pythonPath: string) {
30+
let cmdFile = pythonPath;
31+
let args = ['-c', 'import sys;print(sys.executable)'];
32+
if (pythonPath.indexOf('bin/env ') >= 0 && !IS_WINDOWS) {
33+
// In case we have pythonPath as '/usr/bin/env python'.
34+
const parts = pythonPath.split(' ').map(part => part.trim()).filter(part => part.length > 0);
35+
cmdFile = parts.shift()!;
36+
args = parts.concat(args);
37+
}
38+
return this.processService.exec(cmdFile, args)
39+
.then(output => output.stdout.trim())
40+
.catch(() => '');
41+
}
5442
private async createShebangCodeLens(document: TextDocument) {
55-
const shebang = await ShebangCodeLensProvider.detectShebang(document);
43+
const shebang = await this.detectShebang(document);
5644
const pythonPath = settings.PythonSettings.getInstance(document.uri).pythonPath;
57-
const resolvedPythonPath = await ShebangCodeLensProvider.getFullyQualifiedPathToInterpreter(pythonPath);
45+
const resolvedPythonPath = await this.getFullyQualifiedPathToInterpreter(pythonPath);
5846
if (!shebang || shebang === resolvedPythonPath) {
5947
return [];
6048
}

src/client/interpreter/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as path from 'path';
22
import { ConfigurationTarget, Disposable, StatusBarAlignment, Uri, window, workspace } from 'vscode';
33
import { PythonSettings } from '../common/configSettings';
4+
import { IProcessService } from '../common/process/types';
45
import { IServiceContainer } from '../ioc/types';
56
import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService';
67
import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory';
@@ -21,7 +22,8 @@ export class InterpreterManager implements Disposable {
2122
const statusBar = window.createStatusBarItem(StatusBarAlignment.Left);
2223
this.interpreterProvider = serviceContainer.get<PythonInterpreterLocatorService>(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE);
2324
const versionService = serviceContainer.get<IInterpreterVersionService>(IInterpreterVersionService);
24-
this.display = new InterpreterDisplay(statusBar, this.interpreterProvider, virtualEnvMgr, versionService);
25+
const processService = serviceContainer.get<IProcessService>(IProcessService);
26+
this.display = new InterpreterDisplay(statusBar, this.interpreterProvider, virtualEnvMgr, versionService, processService);
2527
this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), versionService);
2628
PythonSettings.getInstance().addListener('change', () => this.onConfigChanged());
2729
this.disposables.push(window.onDidChangeActiveTextEditor(() => this.refresh()));

src/client/interpreter/locators/services/condaEnvService.ts

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import * as child_process from 'child_process';
21
import * as fs from 'fs-extra';
32
import { inject, injectable } from 'inversify';
43
import * as path from 'path';
54
import { Uri } from 'vscode';
5+
import { IProcessService } from '../../../common/process/types';
66
import { VersionUtils } from '../../../common/versionUtils';
77
import { ICondaLocatorService, IInterpreterLocatorService, IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../contracts';
88
import { AnacondaCompanyName, AnacondaCompanyNames, CONDA_RELATIVE_PY_PATH, CondaInfo } from './conda';
@@ -12,7 +12,8 @@ import { CondaHelper } from './condaHelper';
1212
export class CondaEnvService implements IInterpreterLocatorService {
1313
private readonly condaHelper = new CondaHelper();
1414
constructor( @inject(ICondaLocatorService) private condaLocator: ICondaLocatorService,
15-
@inject(IInterpreterVersionService) private versionService: IInterpreterVersionService) {
15+
@inject(IInterpreterVersionService) private versionService: IInterpreterVersionService,
16+
@inject(IProcessService) private processService: IProcessService) {
1617
}
1718
public async getInterpreters(resource?: Uri) {
1819
return this.getSuggestionsFromConda();
@@ -99,32 +100,24 @@ export class CondaEnvService implements IInterpreterLocatorService {
99100
}
100101
private async getSuggestionsFromConda(): Promise<PythonInterpreter[]> {
101102
return this.condaLocator.getCondaFile()
102-
.then(async condaFile => {
103-
return new Promise<PythonInterpreter[]>((resolve, reject) => {
104-
// interrogate conda (if it's on the path) to find all environments.
105-
child_process.execFile(condaFile, ['info', '--json'], (_, stdout) => {
106-
if (stdout.length === 0) {
107-
resolve([]);
108-
return;
109-
}
103+
.then(condaFile => this.processService.exec(condaFile, ['info', '--json']))
104+
.then(output => output.stdout)
105+
.then(stdout => {
106+
if (stdout.length === 0) {
107+
return [];
108+
}
110109

111-
try {
112-
// tslint:disable-next-line:prefer-type-cast
113-
const info = JSON.parse(stdout) as CondaInfo;
114-
resolve(this.parseCondaInfo(info));
115-
} catch (e) {
116-
// Failed because either:
117-
// 1. conda is not installed.
118-
// 2. `conda info --json` has changed signature.
119-
// 3. output of `conda info --json` has changed in structure.
120-
// In all cases, we can't offer conda pythonPath suggestions.
121-
resolve([]);
122-
}
123-
});
124-
}).catch((err) => {
125-
console.error('Python Extension (getSuggestionsFromConda):', err);
110+
try {
111+
const info = JSON.parse(stdout) as CondaInfo;
112+
return this.parseCondaInfo(info);
113+
} catch {
114+
// Failed because either:
115+
// 1. conda is not installed.
116+
// 2. `conda info --json` has changed signature.
117+
// 3. output of `conda info --json` has changed in structure.
118+
// In all cases, we can't offer conda pythonPath suggestions.
126119
return [];
127-
});
128-
});
120+
}
121+
}).catch(() => []);
129122
}
130123
}

src/client/interpreter/locators/services/condaLocator.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as child_process from 'child_process';
21
import * as fs from 'fs-extra';
32
import { inject, injectable, named, optional } from 'inversify';
43
import * as path from 'path';
@@ -70,15 +69,9 @@ export class CondaLocatorService implements ICondaLocatorService {
7069
}
7170
}
7271
public async isCondaInCurrentPath() {
73-
return new Promise<boolean>((resolve, reject) => {
74-
child_process.execFile('conda', ['--version'], (_, stdout) => {
75-
if (stdout && stdout.length > 0) {
76-
resolve(true);
77-
} else {
78-
resolve(false);
79-
}
80-
});
81-
});
72+
return this.processService.exec('conda', ['--version'])
73+
.then(output => output.stdout.length > 0)
74+
.catch(() => false);
8275
}
8376
private async getCondaFileFromKnownLocations(): Promise<string> {
8477
const condaFiles = await Promise.all(KNOWN_CONDA_LOCATIONS

0 commit comments

Comments
 (0)