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

fix #1147 (search environments.txt file for conda environments) #1240

Merged
merged 9 commits into from
Sep 26, 2017
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { PythonInterpreter } from "../index";
import { Architecture } from "../../common/registry";
import { Architecture } from "../common/registry";

export interface IInterpreterProvider {
export interface IInterpreterLocatorService {
getInterpreters(): Promise<PythonInterpreter[]>;
}
export interface PythonInterpreter {
Expand Down
20 changes: 12 additions & 8 deletions src/client/interpreter/display/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
'use strict';
import * as path from 'path';
import * as utils from '../../common/utils';
import * as child_process from 'child_process';
import { StatusBarItem, Disposable } from 'vscode';
import { PythonSettings } from '../../common/configSettings';
import * as path from 'path';
import { EOL } from 'os';
import { IInterpreterProvider } from '../index';
import * as utils from '../../common/utils';
import { IInterpreterLocatorService } from '../contracts';
import { IInterpreterVersionService } from '../interpreterVersion';
import { VirtualEnvironmentManager } from '../virtualEnvs/index';
import { getFirstNonEmptyLineFromMultilineString } from '../sources/helpers';
import * as child_process from 'child_process';
import { getFirstNonEmptyLineFromMultilineString } from '../helpers';

const settings = PythonSettings.getInstance();
export class InterpreterDisplay implements Disposable {
constructor(private statusBar: StatusBarItem, private interpreterProvoder: IInterpreterProvider, private virtualEnvMgr: VirtualEnvironmentManager) {
constructor(private statusBar: StatusBarItem,
private interpreterLocator: IInterpreterLocatorService,
private virtualEnvMgr: VirtualEnvironmentManager,
private versionProvider: IInterpreterVersionService) {
this.statusBar.command = 'python.setInterpreter';
}
public dispose() {
Expand All @@ -21,7 +25,7 @@ export class InterpreterDisplay implements Disposable {
await this.updateDisplay(pythonPath);
}
private getInterpreters() {
return this.interpreterProvoder.getInterpreters();
return this.interpreterLocator.getInterpreters();
}
private async updateDisplay(pythonPath: string) {
const interpreters = await this.getInterpreters();
Expand All @@ -39,7 +43,7 @@ export class InterpreterDisplay implements Disposable {
else {
const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`;
const interpreterExists = utils.fsExistsAsync(pythonPath);
const displayName = utils.getInterpreterDisplayName(pythonPath).catch(() => defaultDisplayName);
const displayName = this.versionProvider.getVersion(pythonPath, defaultDisplayName);
const virtualEnvName = this.getVirtualEnvironmentName(pythonPath);
await Promise.all([interpreterExists, displayName, virtualEnvName])
.then(([interpreterExists, displayName, virtualEnvName]) => {
Expand Down
7 changes: 7 additions & 0 deletions src/client/interpreter/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function getFirstNonEmptyLineFromMultilineString(stdout: string) {
if (!stdout) {
return '';
}
const lines = stdout.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0);
return lines.length > 0 ? lines[0] : '';
}
17 changes: 9 additions & 8 deletions src/client/interpreter/index.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
'use strict';
import { InterpreterVersionService } from './interpreterVersion';
import { VirtualEnv } from './virtualEnvs/virtualEnv';
import { VEnv } from './virtualEnvs/venv';
import { Disposable, window, StatusBarAlignment, workspace } from 'vscode';
import { PythonSettings } from '../common/configSettings';
import { InterpreterDisplay } from './display';
import { PythonInterpreterProvider } from './sources';
import { PythonInterpreterLocatorService } from './locators';
import { VirtualEnvironmentManager } from './virtualEnvs/index';
import { IS_WINDOWS } from '../common/utils';
import * as path from 'path';
export * from './sources';

const settings = PythonSettings.getInstance();

export class InterpreterManager implements Disposable {
private disposables: Disposable[] = [];
private display: InterpreterDisplay | null | undefined;
private interpreterProvider: PythonInterpreterProvider;
private interpreterProvider: PythonInterpreterLocatorService;
constructor() {
const virtualEnvMgr = new VirtualEnvironmentManager([new VEnv(), new VirtualEnv()]);
const statusBar = window.createStatusBarItem(StatusBarAlignment.Left);
this.interpreterProvider = new PythonInterpreterProvider(virtualEnvMgr);
this.display = new InterpreterDisplay(statusBar, this.interpreterProvider, virtualEnvMgr);
this.interpreterProvider = new PythonInterpreterLocatorService(virtualEnvMgr);
const versionService = new InterpreterVersionService();
this.display = new InterpreterDisplay(statusBar, this.interpreterProvider, virtualEnvMgr, versionService);
settings.addListener('change', this.onConfigChanged.bind(this));
this.display.refresh();

Expand All @@ -41,9 +42,9 @@ export class InterpreterManager implements Disposable {
return;
}

// Ensure this new environment is at the same level as the current workspace
// In windows the interpreter is under scripts/python.exe on linux it is under bin/python
// Meaning the sub directory must be either scripts, bin or other (but only one level deep)
// Ensure this new environment is at the same level as the current workspace.
// In windows the interpreter is under scripts/python.exe on linux it is under bin/python.
// Meaning the sub directory must be either scripts, bin or other (but only one level deep).
const pythonPath = interpretersInWorkspace[0].path;
const relativePath = path.dirname(pythonPath).substring(workspace.rootPath!.length);
if (relativePath.split(path.sep).filter(l => l.length > 0).length === 2) {
Expand Down
13 changes: 13 additions & 0 deletions src/client/interpreter/interpreterVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { getInterpreterDisplayName } from '../common/utils';

export interface IInterpreterVersionService {
getVersion(pythonPath: string, defaultValue: string): Promise<string>;
}

export class InterpreterVersionService implements IInterpreterVersionService {
getVersion(pythonPath: string, defaultValue: string): Promise<string> {
return getInterpreterDisplayName(pythonPath)
.catch(() => defaultValue);
}
}

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PythonInterpreter } from "../index";
import { PythonInterpreter } from "../contracts";
import { IS_WINDOWS, fsReaddirAsync } from "../../common/utils";
import * as path from 'path';
import { getArchitectureDislayName } from "../../common/registry";
Expand All @@ -19,17 +19,8 @@ export function fixInterpreterDisplayName(item: PythonInterpreter) {
return item;
}
export function fixInterpreterPath(item: PythonInterpreter) {
// For some reason anaconda seems to use \\ in the registry path
// For some reason anaconda seems to use \\ in the registry path.
item.path = IS_WINDOWS ? item.path.replace(/\\\\/g, "\\") : item.path;
// Also ensure paths have back slashes instead of forward
item.path = IS_WINDOWS ? item.path.replace(/\//g, "\\") : item.path;
return item;
}

export function getFirstNonEmptyLineFromMultilineString(stdout: string) {
if (stdout.length === 0) {
return '';
}
const lines = stdout.split(/\r?\n/g).filter(line => line.trim().length > 0);
return lines.length > 0 ? lines[0] : '';
}
61 changes: 61 additions & 0 deletions src/client/interpreter/locators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use strict";
import * as _ from 'lodash';
import { fixInterpreterPath, fixInterpreterDisplayName } from './helpers';
import { IInterpreterLocatorService, PythonInterpreter } from '../contracts';
import { InterpreterVersionService } from '../interpreterVersion';
import { IS_WINDOWS, Is_64Bit, arePathsSame, areBasePathsSame } from '../../common/utils';
import { RegistryImplementation } from '../../common/registry';
import { CondaEnvService } from './services/condaEnvService';
import { VirtualEnvService, getKnownSearchPathsForVirtualEnvs } from './services/virtualEnvService';
import { KnownPathsService, getKnownSearchPathsForInterpreters } from './services/KnownPathsService';
import { CurrentPathService } from './services/currentPathService';
import { WindowsRegistryService } from './services/windowsRegistryService';
import { VirtualEnvironmentManager } from '../virtualEnvs';
import { CondaEnvFileService, getEnvironmentsFile as getCondaEnvFile } from './services/condaEnvFileService';

export class PythonInterpreterLocatorService implements IInterpreterLocatorService {
private interpreters: PythonInterpreter[] = [];
private locators: IInterpreterLocatorService[] = [];
constructor(private virtualEnvMgr: VirtualEnvironmentManager) {
const versionService = new InterpreterVersionService();
// The order of the services is important.
if (IS_WINDOWS) {
const windowsRegistryProvider = new WindowsRegistryService(new RegistryImplementation(), Is_64Bit);
this.locators.push(windowsRegistryProvider);
this.locators.push(new CondaEnvService(windowsRegistryProvider));
}
else {
this.locators.push(new CondaEnvService());
}
// Supplements the above list of conda environments.
this.locators.push(new CondaEnvFileService(getCondaEnvFile(), versionService));
this.locators.push(new VirtualEnvService(getKnownSearchPathsForVirtualEnvs(), this.virtualEnvMgr, versionService));

if (!IS_WINDOWS) {
// This must be last, it is possible we have paths returned here that are already returned
// in one of the above lists.
this.locators.push(new KnownPathsService(getKnownSearchPathsForInterpreters(), versionService));
}
// This must be last, it is possible we have paths returned here that are already returned
// in one of the above lists.
this.locators.push(new CurrentPathService(this.virtualEnvMgr, versionService));
}
public async getInterpreters() {
if (this.interpreters.length > 0) {
return this.interpreters;
}
const promises = this.locators.map(provider => provider.getInterpreters());
return Promise.all(promises)
.then(interpreters => _.flatten(interpreters))
.then(items => items.map(fixInterpreterDisplayName))
.then(items => items.map(fixInterpreterPath))
.then(items => items.reduce<PythonInterpreter[]>((accumulator, current) => {
if (accumulator.findIndex(item => arePathsSame(item.path, current.path)) === -1 &&
accumulator.findIndex(item => areBasePathsSame(item.path, current.path)) === -1) {
accumulator.push(current);
}
return accumulator;
}, []))
.then(interpreters => this.interpreters = interpreters);
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"use strict";
import * as path from 'path';
import * as _ from 'lodash';
import { IInterpreterProvider } from '../contracts';
import { fsExistsAsync, getInterpreterDisplayName, IS_WINDOWS } from '../../../common/utils';
import { IInterpreterLocatorService } from '../../contracts';
import { IInterpreterVersionService } from '../../interpreterVersion';
import { fsExistsAsync, IS_WINDOWS } from '../../../common/utils';
import { lookForInterpretersInDirectory } from '../helpers';
const untildify = require('untildify');

export class KnownPathsProvider implements IInterpreterProvider {
public constructor(private knownSearchPaths: string[]) { }
export class KnownPathsService implements IInterpreterLocatorService {
public constructor(private knownSearchPaths: string[],
private versionProvider: IInterpreterVersionService) { }
public getInterpreters() {
return this.suggestionsFromKnownPaths();
}
Expand All @@ -20,7 +22,7 @@ export class KnownPathsProvider implements IInterpreterProvider {
.then(interpreters => Promise.all(interpreters.map(interpreter => this.getInterpreterDetails(interpreter))));
}
private getInterpreterDetails(interpreter: string) {
return getInterpreterDisplayName(interpreter).catch(() => path.basename(interpreter))
return this.versionProvider.getVersion(interpreter, path.basename(interpreter))
.then(displayName => {
return {
displayName,
Expand All @@ -42,7 +44,7 @@ export function getKnownSearchPathsForInterpreters(): string[] {
paths.forEach(p => {
paths.push(untildify('~' + p));
});
// Add support for paths such as /Users/xxx/anaconda/bin
// Add support for paths such as /Users/xxx/anaconda/bin.
if (process.env['HOME']) {
paths.push(path.join(process.env['HOME'], 'anaconda', 'bin'));
paths.push(path.join(process.env['HOME'], 'python', 'bin'));
Expand Down
7 changes: 7 additions & 0 deletions src/client/interpreter/locators/services/conda.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IS_WINDOWS } from "../../../common/utils";

// where to find the Python binary within a conda env.
export const CONDA_RELATIVE_PY_PATH = IS_WINDOWS ? ['python.exe'] : ['bin', 'python'];
export const AnacondaCompanyNames = ['Anaconda, Inc.', 'Continuum Analytics, Inc.'];
export const AnacondaCompanyName = 'Anaconda, Inc.';
export const AnacondaDisplayName = 'Anaconda';
66 changes: 66 additions & 0 deletions src/client/interpreter/locators/services/condaEnvFileService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use strict";
import * as fs from 'fs-extra';
import * as path from 'path';
import { IS_WINDOWS } from '../../../common/configSettings';
import { IInterpreterVersionService } from '../../interpreterVersion';
import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts';
import { AnacondaDisplayName, AnacondaCompanyName, AnacondaCompanyNames, CONDA_RELATIVE_PY_PATH } from './conda';

export class CondaEnvFileService implements IInterpreterLocatorService {
constructor(private condaEnvironmentFile: string,
private versionService: IInterpreterVersionService) {
}
public getInterpreters() {
return this.getSuggestionsFromConda();
}

private getSuggestionsFromConda(): Promise<PythonInterpreter[]> {
return fs.pathExists(this.condaEnvironmentFile)
.then(exists => exists ? this.getEnvironmentsFromFile(this.condaEnvironmentFile) : Promise.resolve([]));
}
private getEnvironmentsFromFile(envFile: string) {
return fs.readFile(envFile)
.then(buffer => buffer.toString().split(/\r?\n/g))
.then(lines => lines.map(line => line.trim()))
.then(lines => lines.map(line => path.join(line, ...CONDA_RELATIVE_PY_PATH)))
.then(interpreterPaths => interpreterPaths.map(item => fs.pathExists(item).then(exists => exists ? item : '')))
.then(promises => Promise.all(promises))
.then(interpreterPaths => interpreterPaths.filter(item => item.length > 0))
.then(interpreterPaths => interpreterPaths.map(item => this.getInterpreterDetails(item)))
.then(promises => Promise.all(promises));
}
private getInterpreterDetails(interpreter: string) {
return this.versionService.getVersion(interpreter, path.basename(interpreter))
.then(version => {
version = this.stripCompanyName(version);
const envName = this.getEnvironmentRootDirectory(interpreter);
const info: PythonInterpreter = {
displayName: `${AnacondaDisplayName} ${version} (${envName})`,
path: interpreter,
companyDisplayName: AnacondaCompanyName,
version: version
};
return info;
});
}
private stripCompanyName(content: string) {
// Strip company name from version.
const startOfCompanyName = AnacondaCompanyNames.reduce((index, companyName) => {
if (index > 0) {
return index;
}
return content.indexOf(`:: ${AnacondaCompanyName}`);
}, -1);

return startOfCompanyName > 0 ? content.substring(0, startOfCompanyName).trim() : content;
}
private getEnvironmentRootDirectory(interpreter: string) {
const envDir = interpreter.substring(0, interpreter.length - path.join(...CONDA_RELATIVE_PY_PATH).length);
return path.basename(envDir);
}
}

export function getEnvironmentsFile() {
const homeDir = IS_WINDOWS ? process.env.USERPROFILE : (process.env.HOME || process.env.HOMEPATH);
return homeDir ? path.join(homeDir, '.conda', 'environments.txt') : '';
}
Loading