Skip to content

Add support for Poetry environments #9399

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

Closed
wants to merge 2 commits into from
Closed
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
5 changes: 5 additions & 0 deletions src/client/interpreter/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const KNOWN_PATH_SERVICE = 'KnownPathsService';
export const GLOBAL_VIRTUAL_ENV_SERVICE = 'VirtualEnvService';
export const WORKSPACE_VIRTUAL_ENV_SERVICE = 'WorkspaceVirtualEnvService';
export const PIPENV_SERVICE = 'PipEnvService';
export const POETRY_ENV_SERVICE = 'PoetryEnvService';
export const IInterpreterVersionService = Symbol('IInterpreterVersionService');
export interface IInterpreterVersionService {
getVersion(pythonPath: string, defaultValue: string): Promise<string>;
Expand Down Expand Up @@ -66,6 +67,7 @@ export interface ICondaService {
export enum InterpreterType {
Unknown = 'Unknown',
Conda = 'Conda',
Poetry = 'Poetry',
VirtualEnv = 'VirtualEnv',
Pipenv = 'PipEnv',
Pyenv = 'Pyenv',
Expand Down Expand Up @@ -124,6 +126,9 @@ export interface IPipEnvService {
isRelatedPipEnvironment(dir: string, pythonPath: string): Promise<boolean>;
}

export const IPoetryEnvService = Symbol('IPoetryEnvService');
export interface IPoetryEnvService {
}
export const IInterpreterLocatorHelper = Symbol('IInterpreterLocatorHelper');
export interface IInterpreterLocatorHelper {
mergeInterpreters(interpreters: PythonInterpreter[]): Promise<PythonInterpreter[]>;
Expand Down
3 changes: 3 additions & 0 deletions src/client/interpreter/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ export class InterpreterHelper implements IInterpreterHelper {
case InterpreterType.VirtualEnv: {
return 'virtualenv';
}
case InterpreterType.Poetry: {
return 'poetry';
}
default: {
return '';
}
Expand Down
4 changes: 3 additions & 1 deletion src/client/interpreter/locators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import {
PIPENV_SERVICE,
PythonInterpreter,
WINDOWS_REGISTRY_SERVICE,
WORKSPACE_VIRTUAL_ENV_SERVICE
WORKSPACE_VIRTUAL_ENV_SERVICE,
POETRY_ENV_SERVICE
} from '../contracts';
import { InterpreterFilter } from './services/interpreterFilter';
import { IInterpreterFilter } from './types';
Expand Down Expand Up @@ -107,6 +108,7 @@ export class PythonInterpreterLocatorService implements IInterpreterLocatorServi
[CONDA_ENV_SERVICE, undefined],
[CONDA_ENV_FILE_SERVICE, undefined],
[PIPENV_SERVICE, undefined],
[POETRY_ENV_SERVICE, undefined],
[GLOBAL_VIRTUAL_ENV_SERVICE, undefined],
[WORKSPACE_VIRTUAL_ENV_SERVICE, undefined],
[KNOWN_PATH_SERVICE, undefined],
Expand Down
71 changes: 71 additions & 0 deletions src/client/interpreter/locators/services/poetryEnvService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { injectable, inject } from 'inversify';
import { IServiceContainer } from '../../../ioc/types';
import { CacheableLocatorService } from './cacheableLocatorService';
import { POETRY_ENV_SERVICE, PythonInterpreter, IInterpreterHelper, InterpreterType } from '../../contracts';
import { PoetryServce } from './poetryService';
import { Resource } from '../../../common/types';
import { traceError, traceDecorators } from '../../../common/logger';
import { noop } from '../../../common/utils/misc';

/**
* Interpreter locator for Poetry.
*
* @export
* @class PoetryEnvService
* @extends {CacheableLocatorService}
*/
@injectable()
export class PoetryEnvService extends CacheableLocatorService {
constructor(
@inject(IServiceContainer) serviceContainer: IServiceContainer,
@inject(PoetryServce) private readonly poetryService: PoetryServce,
@inject(IInterpreterHelper) private readonly helper: IInterpreterHelper
) {
super(POETRY_ENV_SERVICE, serviceContainer, true);
}
public dispose(): void {
noop();
}
protected async getInterpretersImplementation(resource: Resource): Promise<PythonInterpreter[]> {
return this.getPoetryInterpreters(resource).catch(ex => {
traceError('Failed to get Poetry Interpreters', ex);
return [];
});
}

@traceDecorators.error('Failed to get Poetry Interepters')
protected async getPoetryInterpreters(resource: Resource): Promise<PythonInterpreter[]> {
if (!(await this.poetryService.isInstalled(resource))) {
return [];
}

const interpreterPaths = await this.poetryService.getEnvironments(resource);
const items = await Promise.all(
interpreterPaths.map(item => {
return this.helper
.getInterpreterInformation(item)
.then(info => {
if (!info) {
return;
}
return {
...info,
type: InterpreterType.Poetry
};
})
.catch(ex => {
// Handle each error, we don't want everything to fail.
traceError(`Failed to get interpreter information for Poetry Interpreter, ${item}`, ex);
return;
});
})
);

return items.filter(item => !!item).map(item => item! as PythonInterpreter);
}
}
102 changes: 102 additions & 0 deletions src/client/interpreter/locators/services/poetryService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { injectable, inject } from 'inversify';
import { IConfigurationService, IDisposable, IDisposableRegistry, Resource } from '../../../common/types';
import { IProcessServiceFactory } from '../../../common/process/types';
import { cache } from '../../../common/utils/decorators';
import { traceError } from '../../../common/logger';
import { IFileSystem } from '../../../common/platform/types';
import { lookForInterpretersInDirectory } from '../helpers';
const flatten = require('lodash/flatten') as typeof import('lodash/flatten');

const cacheEnvDuration = 10 * 60 * 1000;
const cacheIsInstalledDuration = 60 * 1000;

@injectable()
export class PoetryServce implements IDisposable {
private readonly disposables: IDisposable[] = [];
constructor(
@inject(IConfigurationService) private readonly configurationService: IConfigurationService,
@inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry,
@inject(IFileSystem) private readonly fs: IFileSystem,
@inject(IProcessServiceFactory) private readonly processFactory: IProcessServiceFactory
) {
disposableRegistry.push(this);
}

public dispose() {
this.disposables.forEach(d => d.dispose());
}
@cache(cacheIsInstalledDuration)
public async isInstalled(resource: Resource): Promise<boolean> {
const processService = await this.processFactory.create(resource);
return processService
.exec(this.configurationService.getSettings(resource).poetryPath, ['--version'])
.then(output => (output.stderr ? false : true))
.catch(() => false);
}

// @cache(cacheEnvDuration)
// public async getCurrentEnvironment(resource: Resource): Promise<string | undefined> {
// const processService = await this.processFactory.create(resource);
// const dir = await processService
// .exec(this.configurationService.getSettings(resource).poetryPath, ['env', 'info', '--path'])
// .then(out => {
// if (out.stderr) {
// traceError('Failed to get current environment from Poetry', out.stderr);
// return '';
// }
// return out.stdout.endsWith('%') ? out.stdout.substring(0, out.stdout.length - 1).trim() : out.stdout.trim();
// })
// .catch(ex => {
// traceError('Failed to get current environment from Poetry', ex);
// return '';
// });

// const interpreters = await this.getInterpretersInDirectory(dir);
// return interpreters.length === 0 ? undefined : interpreters[0];
// }
@cache(cacheEnvDuration)
public async getEnvironments(resource: Resource): Promise<string[]> {
const processService = await this.processFactory.create(resource);
const output = await processService
.exec(this.configurationService.getSettings(resource).poetryPath, ['env', 'list', '--full-path'])
.then(out => {
if (out.stderr) {
traceError('Failed to get a list of environments from Poetry', out.stderr);
return '';
}
return out.stdout;
})
.catch(ex => {
traceError('Failed to get a list of environments from Poetry', ex);
return '';
});

const interpreters = output
.splitLines({ trim: true, removeEmptyEntries: true })
.map(line => {
if (line.endsWith('(Activated)')) {
return line.substring(0, line.length - '(Activated)'.length).trim();
}
return line;
})
.map(dir => this.getInterpretersInDirectory(dir));

return Promise.all(interpreters).then(listOfInterpreters => flatten(listOfInterpreters));
}

/**
* Return the interpreters in the given directory.
*/
private async getInterpretersInDirectory(dir: string) {
const exists = this.fs.directoryExists(dir);
if (exists) {
return lookForInterpretersInDirectory(dir, this.fs);
}
return [];
}
}
5 changes: 4 additions & 1 deletion src/client/interpreter/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ import {
KNOWN_PATH_SERVICE,
PIPENV_SERVICE,
WINDOWS_REGISTRY_SERVICE,
WORKSPACE_VIRTUAL_ENV_SERVICE
WORKSPACE_VIRTUAL_ENV_SERVICE,
POETRY_ENV_SERVICE
} from './contracts';
import { InterpreterDisplay } from './display';
import { InterpreterSelectionTip } from './display/interpreterSelectionTip';
Expand Down Expand Up @@ -78,6 +79,7 @@ import { CondaInheritEnvPrompt } from './virtualEnvs/condaInheritEnvPrompt';
import { VirtualEnvironmentManager } from './virtualEnvs/index';
import { IVirtualEnvironmentManager } from './virtualEnvs/types';
import { VirtualEnvironmentPrompt } from './virtualEnvs/virtualEnvPrompt';
import { PoetryEnvService } from './locators/services/poetryEnvService';

export function registerTypes(serviceManager: IServiceManager) {
serviceManager.addSingleton<IKnownSearchPathsForInterpreters>(IKnownSearchPathsForInterpreters, KnownSearchPathsForInterpreters);
Expand All @@ -102,6 +104,7 @@ export function registerTypes(serviceManager: IServiceManager) {
serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, GlobalVirtualEnvService, GLOBAL_VIRTUAL_ENV_SERVICE);
serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, WorkspaceVirtualEnvService, WORKSPACE_VIRTUAL_ENV_SERVICE);
serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, PipEnvService, PIPENV_SERVICE);
serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, PoetryEnvService, POETRY_ENV_SERVICE);
serviceManager.addSingleton<IInterpreterLocatorService>(IPipEnvService, PipEnvService);

serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, WindowsRegistryService, WINDOWS_REGISTRY_SERVICE);
Expand Down
5 changes: 4 additions & 1 deletion src/test/datascience/dataScienceIocContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ import {
PIPENV_SERVICE,
PythonInterpreter,
WINDOWS_REGISTRY_SERVICE,
WORKSPACE_VIRTUAL_ENV_SERVICE
WORKSPACE_VIRTUAL_ENV_SERVICE,
POETRY_ENV_SERVICE
} from '../../client/interpreter/contracts';
import { ShebangCodeLensProvider } from '../../client/interpreter/display/shebangCodeLensProvider';
import { InterpreterHelper } from '../../client/interpreter/helpers';
Expand Down Expand Up @@ -268,6 +269,7 @@ import { MockWorkspaceConfiguration } from './mockWorkspaceConfig';
import { blurWindow, createMessageEvent } from './reactHelpers';
import { TestInteractiveWindowProvider } from './testInteractiveWindowProvider';
import { TestNativeEditorProvider } from './testNativeEditorProvider';
import { PoetryEnvService } from '../../client/interpreter/locators/services/poetryEnvService';

export class DataScienceIocContainer extends UnitTestIocContainer {
public webPanelListener: IWebPanelMessageListener | undefined;
Expand Down Expand Up @@ -632,6 +634,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer {
this.serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, GlobalVirtualEnvService, GLOBAL_VIRTUAL_ENV_SERVICE);
this.serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, WorkspaceVirtualEnvService, WORKSPACE_VIRTUAL_ENV_SERVICE);
this.serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, PipEnvService, PIPENV_SERVICE);
this.serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, PoetryEnvService, POETRY_ENV_SERVICE);
this.serviceManager.addSingleton<IInterpreterLocatorService>(IPipEnvService, PipEnvService);
this.serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, WindowsRegistryService, WINDOWS_REGISTRY_SERVICE);

Expand Down
75 changes: 75 additions & 0 deletions src/test/interpreters/locators/poetryEnvService.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import * as path from 'path';
import { PoetryServce } from '../../../client/interpreter/locators/services/poetryService';
import { Uri } from 'vscode';
import { assert } from 'chai';
import { when, mock, instance, verify } from 'ts-mockito';
import { Resource } from '../../../client/common/types';
import { PoetryEnvService } from '../../../client/interpreter/locators/services/poetryEnvService';
import { IInterpreterHelper, InterpreterType } from '../../../client/interpreter/contracts';
import { ServiceContainer } from '../../../client/ioc/container';
import { InterpreterHelper } from '../../../client/interpreter/helpers';
import { IServiceContainer } from '../../../client/ioc/types';

suite('Interpreters - PoetryService', () => {
class PoetryEnvServiceTest extends PoetryEnvService {
public getInterpretersImplementation(resource: Resource) {
return super.getInterpretersImplementation(resource);
}
}
let poetryService: PoetryServce;
let poetryEnvService: PoetryEnvServiceTest;
let svc: IServiceContainer;
let helper: IInterpreterHelper;
setup(() => {
poetryService = mock(PoetryServce);
svc = mock(ServiceContainer);
helper = mock(InterpreterHelper);

poetryEnvService = new PoetryEnvServiceTest(instance(svc), instance(poetryService), instance(helper));
});

[undefined, Uri.file('wow.py')].forEach(resource => {
suite(resource ? 'Without a resource' : 'With a resource', () => {
test('Returns an empty list of interpreters if poetry is not installed', async () => {
when(poetryService.isInstalled(resource)).thenResolve(false);

const interpreters = await poetryEnvService.getInterpretersImplementation(resource);

verify(poetryService.isInstalled(resource)).once();
verify(poetryService.getEnvironments(resource)).never();
assert.deepEqual(interpreters, []);
});
test('Returns an empty list of interpreters if no environments are returned by PoetryService', async () => {
when(poetryService.isInstalled(resource)).thenResolve(true);
when(poetryService.getEnvironments(resource)).thenResolve([]);

const interpreters = await poetryEnvService.getInterpretersImplementation(resource);

verify(poetryService.isInstalled(resource)).once();
verify(poetryService.getEnvironments(resource)).once();
assert.deepEqual(interpreters, []);
});
test('Returns an list of interpreters', async () => {
const envs = [path.join('one', 'wow.exe'), path.join('two', 'python')];
when(poetryService.isInstalled(resource)).thenResolve(true);
when(poetryService.getEnvironments(resource)).thenResolve(envs);
when(helper.getInterpreterInformation(envs[0])).thenResolve({ path: envs[0] });
when(helper.getInterpreterInformation(envs[1])).thenResolve({ path: envs[1] });

const interpreters = await poetryEnvService.getInterpretersImplementation(resource);

verify(poetryService.isInstalled(resource)).once();
verify(poetryService.getEnvironments(resource)).once();
assert.deepEqual(
interpreters,
envs.map(item => ({ path: item, type: InterpreterType.Poetry }))
);
});
});
});
});
Loading