Skip to content

Commit

Permalink
Complete refactoring of code to use new code exec engine (#493)
Browse files Browse the repository at this point in the history
Fixes #491
  • Loading branch information
DonJayamanne authored Jan 3, 2018
1 parent b1a3992 commit f82af7a
Show file tree
Hide file tree
Showing 27 changed files with 279 additions and 267 deletions.
4 changes: 2 additions & 2 deletions src/client/common/process/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export type ExecutionResult<T extends string | Buffer> = {
export const IProcessService = Symbol('IProcessService');

export interface IProcessService {
execObservable(file: string, args: string[], options: SpawnOptions): ObservableExecutionResult<string>;
exec(file: string, args: string[], options: SpawnOptions): Promise<ExecutionResult<string>>;
execObservable(file: string, args: string[], options?: SpawnOptions): ObservableExecutionResult<string>;
exec(file: string, args: string[], options?: SpawnOptions): Promise<ExecutionResult<string>>;
}

export const IPythonExecutionFactory = Symbol('IPythonExecutionFactory');
Expand Down
3 changes: 2 additions & 1 deletion src/client/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ export enum Product {
mypy = 11,
unittest = 12,
ctags = 13,
rope = 14
rope = 14,
isort = 15
}

export enum ModuleNamePurpose {
Expand Down
13 changes: 0 additions & 13 deletions src/client/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';
// 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

import * as child_process from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
Expand Down Expand Up @@ -30,18 +29,6 @@ export function fsReaddirAsync(root: string): Promise<string[]> {
});
}

export async function getPathFromPythonCommand(pythonPath: string): Promise<string> {
return await new Promise<string>((resolve, reject) => {
child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => {
if (stdout) {
const lines = stdout.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0);
resolve(lines.length > 0 ? lines[0] : '');
} else {
reject();
}
});
});
}
export function formatErrorForLogging(error: Error | string): string {
let message: string = '';
if (typeof error === 'string') {
Expand Down
10 changes: 6 additions & 4 deletions src/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { STANDARD_OUTPUT_CHANNEL } from './common/constants';
import { FeatureDeprecationManager } from './common/featureDeprecationManager';
import { createDeferred } from './common/helpers';
import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry';
import { IProcessService, IPythonExecutionFactory } from './common/process/types';
import { registerTypes as commonRegisterTypes } from './common/serviceRegistry';
import { GLOBAL_MEMENTO, IDisposableRegistry, ILogger, IMemento, IOutputChannel, IPersistentStateFactory, WORKSPACE_MEMENTO } from './common/types';
import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry';
Expand Down Expand Up @@ -85,23 +86,24 @@ export async function activate(context: vscode.ExtensionContext) {
const pythonSettings = settings.PythonSettings.getInstance();
sendStartupTelemetry(activated, serviceContainer);

sortImports.activate(context, standardOutputChannel);
sortImports.activate(context, standardOutputChannel, serviceContainer);
const interpreterManager = new InterpreterManager(serviceContainer);
// This must be completed before we can continue.
await interpreterManager.autoSetInterpreter();

interpreterManager.refresh()
.catch(ex => console.error('Python Extension: interpreterManager.refresh', ex));
context.subscriptions.push(interpreterManager);
const processService = serviceContainer.get<IProcessService>(IProcessService);
const interpreterVersionService = serviceContainer.get<IInterpreterVersionService>(IInterpreterVersionService);
context.subscriptions.push(new SetInterpreterProvider(interpreterManager, interpreterVersionService));
context.subscriptions.push(new SetInterpreterProvider(interpreterManager, interpreterVersionService, processService));
context.subscriptions.push(...activateExecInTerminalProvider());
context.subscriptions.push(activateUpdateSparkLibraryProvider());
activateSimplePythonRefactorProvider(context, standardOutputChannel, serviceContainer);
const jediFactory = new JediFactory(context.asAbsolutePath('.'), serviceContainer);
context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory));

context.subscriptions.push(new ReplProvider());
context.subscriptions.push(new ReplProvider(serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory)));

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

const symbolProvider = new PythonSymbolProvider(jediFactory);
context.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(PYTHON, symbolProvider));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as path from 'path';
import { commands, ConfigurationTarget, Disposable, QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode';
import { InterpreterManager } from '../';
import * as settings from '../../common/configSettings';
import { IProcessService } from '../../common/process/types';
import { IInterpreterVersionService, PythonInterpreter, WorkspacePythonPath } from '../contracts';
import { ShebangCodeLensProvider } from '../display/shebangCodeLensProvider';
import { PythonPathUpdaterService } from './pythonPathUpdaterService';
Expand All @@ -15,7 +16,9 @@ interface PythonPathQuickPickItem extends QuickPickItem {
export class SetInterpreterProvider implements Disposable {
private disposables: Disposable[] = [];
private pythonPathUpdaterService: PythonPathUpdaterService;
constructor(private interpreterManager: InterpreterManager, interpreterVersionService: IInterpreterVersionService) {
constructor(private interpreterManager: InterpreterManager,
interpreterVersionService: IInterpreterVersionService,
private processService: IProcessService) {
this.disposables.push(commands.registerCommand('python.setInterpreter', this.setInterpreter.bind(this)));
this.disposables.push(commands.registerCommand('python.setShebangInterpreter', this.setShebangInterpreter.bind(this)));
this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), interpreterVersionService);
Expand Down Expand Up @@ -88,7 +91,7 @@ export class SetInterpreterProvider implements Disposable {
}

private async setShebangInterpreter(): Promise<void> {
const shebang = await ShebangCodeLensProvider.detectShebang(window.activeTextEditor!.document);
const shebang = await new ShebangCodeLensProvider(this.processService).detectShebang(window.activeTextEditor!.document);
if (!shebang) {
return;
}
Expand Down
14 changes: 6 additions & 8 deletions src/client/interpreter/display/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import * as child_process from 'child_process';
import { EOL } from 'os';
import * as path from 'path';
import { Disposable, StatusBarItem, Uri } from 'vscode';
import { PythonSettings } from '../../common/configSettings';
import { IProcessService } from '../../common/process/types';
import * as utils from '../../common/utils';
import { IInterpreterLocatorService, IInterpreterVersionService } from '../contracts';
import { getActiveWorkspaceUri, getFirstNonEmptyLineFromMultilineString } from '../helpers';
import { getActiveWorkspaceUri } from '../helpers';
import { IVirtualEnvironmentManager } from '../virtualEnvs/types';

// tslint:disable-next-line:completed-docs
export class InterpreterDisplay implements Disposable {
constructor(private statusBar: StatusBarItem,
private interpreterLocator: IInterpreterLocatorService,
private virtualEnvMgr: IVirtualEnvironmentManager,
private versionProvider: IInterpreterVersionService) {
private versionProvider: IInterpreterVersionService,
private processService: IProcessService) {

this.statusBar.command = 'python.setInterpreter';
}
Expand Down Expand Up @@ -69,11 +70,8 @@ export class InterpreterDisplay implements Disposable {
.then(env => env ? env.name : '');
}
private async getFullyQualifiedPathToInterpreter(pythonPath: string) {
return new Promise<string>(resolve => {
child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => {
resolve(getFirstNonEmptyLineFromMultilineString(stdout));
});
})
return this.processService.exec(pythonPath, ['-c', 'import sys;print(sys.executable)'])
.then(output => output.stdout.trim())
.then(value => value.length === 0 ? pythonPath : value)
.catch(() => pythonPath);
}
Expand Down
52 changes: 20 additions & 32 deletions src/client/interpreter/display/shebangCodeLensProvider.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
'use strict';
import * as child_process from 'child_process';
import * as vscode from 'vscode';
import { CancellationToken, CodeLens, TextDocument } from 'vscode';
import * as settings from '../../common/configSettings';
import { IProcessService } from '../../common/process/types';
import { IS_WINDOWS } from '../../common/utils';
import { getFirstNonEmptyLineFromMultilineString } from '../../interpreter/helpers';

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

const shebang = firstLine.text.substr(2).trim();
const pythonPath = await ShebangCodeLensProvider.getFullyQualifiedPathToInterpreter(shebang);
const pythonPath = await this.getFullyQualifiedPathToInterpreter(shebang);
return typeof pythonPath === 'string' && pythonPath.length > 0 ? pythonPath : undefined;
}
private static async getFullyQualifiedPathToInterpreter(pythonPath: string) {
if (pythonPath.indexOf('bin/env ') >= 0 && !IS_WINDOWS) {
// In case we have pythonPath as '/usr/bin/env python'
return new Promise<string>(resolve => {
const command = child_process.exec(`${pythonPath} -c 'import sys;print(sys.executable)'`);
let result = '';
command.stdout.on('data', (data) => {
result += data.toString();
});
command.on('close', () => {
resolve(getFirstNonEmptyLineFromMultilineString(result));
});
});
} else {
return new Promise<string>(resolve => {
child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => {
resolve(getFirstNonEmptyLineFromMultilineString(stdout));
});
});
}
}

public async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise<CodeLens[]> {
const codeLenses = await this.createShebangCodeLens(document);
return Promise.resolve(codeLenses);
}

private async getFullyQualifiedPathToInterpreter(pythonPath: string) {
let cmdFile = pythonPath;
let args = ['-c', 'import sys;print(sys.executable)'];
if (pythonPath.indexOf('bin/env ') >= 0 && !IS_WINDOWS) {
// In case we have pythonPath as '/usr/bin/env python'.
const parts = pythonPath.split(' ').map(part => part.trim()).filter(part => part.length > 0);
cmdFile = parts.shift()!;
args = parts.concat(args);
}
return this.processService.exec(cmdFile, args)
.then(output => output.stdout.trim())
.catch(() => '');
}
private async createShebangCodeLens(document: TextDocument) {
const shebang = await ShebangCodeLensProvider.detectShebang(document);
const shebang = await this.detectShebang(document);
const pythonPath = settings.PythonSettings.getInstance(document.uri).pythonPath;
const resolvedPythonPath = await ShebangCodeLensProvider.getFullyQualifiedPathToInterpreter(pythonPath);
const resolvedPythonPath = await this.getFullyQualifiedPathToInterpreter(pythonPath);
if (!shebang || shebang === resolvedPythonPath) {
return [];
}
Expand Down
4 changes: 3 additions & 1 deletion src/client/interpreter/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as path from 'path';
import { ConfigurationTarget, Disposable, StatusBarAlignment, Uri, window, workspace } from 'vscode';
import { PythonSettings } from '../common/configSettings';
import { IProcessService } from '../common/process/types';
import { IServiceContainer } from '../ioc/types';
import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService';
import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory';
Expand All @@ -21,7 +22,8 @@ export class InterpreterManager implements Disposable {
const statusBar = window.createStatusBarItem(StatusBarAlignment.Left);
this.interpreterProvider = serviceContainer.get<PythonInterpreterLocatorService>(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE);
const versionService = serviceContainer.get<IInterpreterVersionService>(IInterpreterVersionService);
this.display = new InterpreterDisplay(statusBar, this.interpreterProvider, virtualEnvMgr, versionService);
const processService = serviceContainer.get<IProcessService>(IProcessService);
this.display = new InterpreterDisplay(statusBar, this.interpreterProvider, virtualEnvMgr, versionService, processService);
this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), versionService);
PythonSettings.getInstance().addListener('change', () => this.onConfigChanged());
this.disposables.push(window.onDidChangeActiveTextEditor(() => this.refresh()));
Expand Down
47 changes: 20 additions & 27 deletions src/client/interpreter/locators/services/condaEnvService.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as child_process from 'child_process';
import * as fs from 'fs-extra';
import { inject, injectable } from 'inversify';
import * as path from 'path';
import { Uri } from 'vscode';
import { IProcessService } from '../../../common/process/types';
import { VersionUtils } from '../../../common/versionUtils';
import { ICondaLocatorService, IInterpreterLocatorService, IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../contracts';
import { AnacondaCompanyName, AnacondaCompanyNames, CONDA_RELATIVE_PY_PATH, CondaInfo } from './conda';
Expand All @@ -12,7 +12,8 @@ import { CondaHelper } from './condaHelper';
export class CondaEnvService implements IInterpreterLocatorService {
private readonly condaHelper = new CondaHelper();
constructor( @inject(ICondaLocatorService) private condaLocator: ICondaLocatorService,
@inject(IInterpreterVersionService) private versionService: IInterpreterVersionService) {
@inject(IInterpreterVersionService) private versionService: IInterpreterVersionService,
@inject(IProcessService) private processService: IProcessService) {
}
public async getInterpreters(resource?: Uri) {
return this.getSuggestionsFromConda();
Expand Down Expand Up @@ -99,32 +100,24 @@ export class CondaEnvService implements IInterpreterLocatorService {
}
private async getSuggestionsFromConda(): Promise<PythonInterpreter[]> {
return this.condaLocator.getCondaFile()
.then(async condaFile => {
return new Promise<PythonInterpreter[]>((resolve, reject) => {
// interrogate conda (if it's on the path) to find all environments.
child_process.execFile(condaFile, ['info', '--json'], (_, stdout) => {
if (stdout.length === 0) {
resolve([]);
return;
}
.then(condaFile => this.processService.exec(condaFile, ['info', '--json']))
.then(output => output.stdout)
.then(stdout => {
if (stdout.length === 0) {
return [];
}

try {
// tslint:disable-next-line:prefer-type-cast
const info = JSON.parse(stdout) as CondaInfo;
resolve(this.parseCondaInfo(info));
} catch (e) {
// Failed because either:
// 1. conda is not installed.
// 2. `conda info --json` has changed signature.
// 3. output of `conda info --json` has changed in structure.
// In all cases, we can't offer conda pythonPath suggestions.
resolve([]);
}
});
}).catch((err) => {
console.error('Python Extension (getSuggestionsFromConda):', err);
try {
const info = JSON.parse(stdout) as CondaInfo;
return this.parseCondaInfo(info);
} catch {
// Failed because either:
// 1. conda is not installed.
// 2. `conda info --json` has changed signature.
// 3. output of `conda info --json` has changed in structure.
// In all cases, we can't offer conda pythonPath suggestions.
return [];
});
});
}
}).catch(() => []);
}
}
13 changes: 3 additions & 10 deletions src/client/interpreter/locators/services/condaLocator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as child_process from 'child_process';
import * as fs from 'fs-extra';
import { inject, injectable, named, optional } from 'inversify';
import * as path from 'path';
Expand Down Expand Up @@ -70,15 +69,9 @@ export class CondaLocatorService implements ICondaLocatorService {
}
}
public async isCondaInCurrentPath() {
return new Promise<boolean>((resolve, reject) => {
child_process.execFile('conda', ['--version'], (_, stdout) => {
if (stdout && stdout.length > 0) {
resolve(true);
} else {
resolve(false);
}
});
});
return this.processService.exec('conda', ['--version'])
.then(output => output.stdout.length > 0)
.catch(() => false);
}
private async getCondaFileFromKnownLocations(): Promise<string> {
const condaFiles = await Promise.all(KNOWN_CONDA_LOCATIONS
Expand Down
Loading

0 comments on commit f82af7a

Please sign in to comment.