Skip to content

Poetry interpreter support #11839

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 8 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
1 change: 1 addition & 0 deletions news/1 Enhancements/11839.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added Poetry interpreter support (thanks [Terry Cain](https://github.com/terrycain))
2 changes: 2 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@
"Interpreters.LoadingInterpreters": "Loading Python Interpreters",
"Interpreters.unsafeInterpreterMessage": "We found a Python environment in this workspace. Do you want to select it to start up the features in the Python extension? Only accept if you trust this environment.",
"Interpreters.condaInheritEnvMessage": "We noticed you're using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we recommend that you let the Python extension change \"terminal.integrated.inheritEnv\" to false in your user settings.",
"Interpreters.poetryVenvMissing" : "Workspace contains pyproject.toml but the associated virtual environment has not been setup. Setup the virtual environment manually if needed.",
"Interpreters.poetryBinaryMissing" : "Workspace contains pyproject.toml but '{0}' was not found. Make sure '{0}' is on the PATH.",
"Logging.CurrentWorkingDirectory": "cwd:",
"InterpreterQuickPickList.quickPickListPlaceholder": "Current: {0}",
"InterpreterQuickPickList.enterPath.detail": "Enter path or find an existing interpreter",
Expand Down
1 change: 1 addition & 0 deletions src/client/common/process/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export type InterpreterInfomation = {
architecture: Architecture;
sysPrefix: string;
pipEnvWorkspaceFolder?: string;
poetryWorkspaceFolder?: string;
};
export const IPythonExecutionService = Symbol('IPythonExecutionService');

Expand Down
9 changes: 9 additions & 0 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@ export namespace Interpreters {
'Tip: you can change the Python interpreter used by the Python extension by clicking on the Python version in the status bar'
);
export const pythonInterpreterPath = localize('Interpreters.pythonInterpreterPath', 'Python interpreter path: {0}');

export const poetryVenvMissing = localize(
'Interpreters.poetryVenvMissing',
'Workspace contains pyproject.toml but the associated virtual environment has not been setup. Setup the virtual environment manually if needed.'
);
export const poetryBinaryMissing = localize(
'Interpreters.poetryBinaryMissing',
"Workspace contains pyproject.toml but '{0}' was not found. Make sure '{0}' is on the PATH."
);
}

export namespace InterpreterQuickPickList {
Expand Down
3 changes: 2 additions & 1 deletion src/client/interpreter/autoSelection/rules/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export class SystemWideInterpretersAutoSelectionRule extends BaseRuleService {
(int) =>
int.type !== InterpreterType.VirtualEnv &&
int.type !== InterpreterType.Venv &&
int.type !== InterpreterType.Pipenv
int.type !== InterpreterType.Pipenv &&
int.type !== InterpreterType.Poetry
);
const bestInterpreter = this.helper.getBestInterpreter(filteredInterpreters);
traceVerbose(
Expand Down
8 changes: 8 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_SERVICE = 'PoetryService';
export const IInterpreterVersionService = Symbol('IInterpreterVersionService');
export interface IInterpreterVersionService {
getVersion(pythonPath: string, defaultValue: string): Promise<string>;
Expand Down Expand Up @@ -76,6 +77,7 @@ export enum InterpreterType {
Conda = 'Conda',
VirtualEnv = 'VirtualEnv',
Pipenv = 'PipEnv',
Poetry = 'Poetry',
Pyenv = 'Pyenv',
Venv = 'Venv',
WindowsStore = 'WindowsStore'
Expand Down Expand Up @@ -132,6 +134,12 @@ export interface IPipEnvService extends IInterpreterLocatorService {
isRelatedPipEnvironment(dir: string, pythonPath: string): Promise<boolean>;
}

export const IPoetryService = Symbol('IPoetryService');
export interface IPoetryService extends IInterpreterLocatorService {
executable: string;
isRelatedPoetryEnvironment(dir: string, pythonPath: string): Promise<boolean>;
}

export const IInterpreterLocatorHelper = Symbol('IInterpreterLocatorHelper');
export interface IInterpreterLocatorHelper {
mergeInterpreters(interpreters: PythonInterpreter[]): Promise<PythonInterpreter[]>;
Expand Down
4 changes: 3 additions & 1 deletion src/client/interpreter/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export function isInterpreterLocatedInWorkspace(interpreter: PythonInterpreter,
const resourcePath = fileSystemPaths.normCase(activeWorkspaceUri.fsPath);
return interpreterPath.startsWith(resourcePath);
}

@injectable()
export class InterpreterHelper implements IInterpreterHelper {
private readonly persistentFactory: IPersistentStateFactory;
Expand Down Expand Up @@ -114,6 +113,9 @@ export class InterpreterHelper implements IInterpreterHelper {
case InterpreterType.Pipenv: {
return 'pipenv';
}
case InterpreterType.Poetry: {
return 'poetry';
}
case InterpreterType.Pyenv: {
return 'pyenv';
}
Expand Down
7 changes: 6 additions & 1 deletion src/client/interpreter/interpreterService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,12 @@ export class InterpreterService implements Disposable, IInterpreterService {
if (info.architecture) {
displayNameParts.push(getArchitectureDisplayName(info.architecture));
}
if (!info.envName && info.path && info.type && info.type === InterpreterType.Pipenv) {
if (
!info.envName &&
info.path &&
info.type &&
(info.type === InterpreterType.Pipenv || info.type === InterpreterType.Poetry)
) {
// If we do not have the name of the environment, then try to get it again.
// This can happen based on the context (i.e. resource).
// I.e. we can determine if an environment is PipEnv only when giving it the right workspacec path (i.e. resource).
Expand Down
24 changes: 22 additions & 2 deletions src/client/interpreter/locators/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as fsapi from 'fs-extra';
import { inject, injectable } from 'inversify';
import * as path from 'path';
import { traceError } from '../../common/logger';
import { traceError, traceWarning } from '../../common/logger';
import { IS_WINDOWS } from '../../common/platform/constants';
import { IFileSystem } from '../../common/platform/types';
import { IFileSystem, IPlatformService } from '../../common/platform/types';
import { ICurrentProcess } from '../../common/types';
import { IInterpreterLocatorHelper, InterpreterType, PythonInterpreter } from '../contracts';
import { IPipEnvServiceHelper } from './types';

Expand All @@ -25,6 +26,22 @@ export async function lookForInterpretersInDirectory(pathToCheck: string, _: IFi
}
}

export function traceProcessError(
platformService: IPlatformService,
currentProcess: ICurrentProcess,
error: Error,
name: string
) {
const enviromentVariableValues: Record<string, string | undefined> = {
LC_ALL: currentProcess.env.LC_ALL,
LANG: currentProcess.env.LANG
};
enviromentVariableValues[platformService.pathVariableName] = currentProcess.env[platformService.pathVariableName];

traceWarning(`Error in invoking ${name}`, error);
traceWarning(`Relevant Environment Variables ${JSON.stringify(enviromentVariableValues, undefined, 4)}`);
}

@injectable()
export class InterpreterLocatorHelper implements IInterpreterLocatorHelper {
constructor(
Expand Down Expand Up @@ -91,6 +108,9 @@ export class InterpreterLocatorHelper implements IInterpreterLocatorHelper {
item.pipEnvWorkspaceFolder = info.workspaceFolder.fsPath;
item.envName = info.envName || item.envName;
}
if (item.path.includes('pypoetry')) {
item.type = InterpreterType.Poetry;
}
})
);
return items;
Expand Down
2 changes: 2 additions & 0 deletions src/client/interpreter/locators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
IInterpreterLocatorService,
KNOWN_PATH_SERVICE,
PIPENV_SERVICE,
POETRY_SERVICE,
PythonInterpreter,
WINDOWS_REGISTRY_SERVICE,
WORKSPACE_VIRTUAL_ENV_SERVICE
Expand Down Expand Up @@ -115,6 +116,7 @@ export class PythonInterpreterLocatorService implements IInterpreterLocatorServi
[CONDA_ENV_SERVICE, undefined],
[CONDA_ENV_FILE_SERVICE, undefined],
[PIPENV_SERVICE, undefined],
[POETRY_SERVICE, undefined],
[GLOBAL_VIRTUAL_ENV_SERVICE, undefined],
[WORKSPACE_VIRTUAL_ENV_SERVICE, undefined],
[KNOWN_PATH_SERVICE, undefined],
Expand Down
13 changes: 3 additions & 10 deletions src/client/interpreter/locators/services/pipEnvService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { inject, injectable } from 'inversify';
import * as path from 'path';
import { Uri } from 'vscode';
import { IApplicationShell, IWorkspaceService } from '../../../common/application/types';
import { traceError, traceWarning } from '../../../common/logger';
import { traceError } from '../../../common/logger';
import { IFileSystem, IPlatformService } from '../../../common/platform/types';
import { IProcessServiceFactory } from '../../../common/process/types';
import { IConfigurationService, ICurrentProcess } from '../../../common/types';
Expand All @@ -20,6 +20,7 @@ import {
IPipEnvService,
PythonInterpreter
} from '../../contracts';
import { traceProcessError } from '../helpers';
import { IPipEnvServiceHelper } from '../types';
import { CacheableLocatorService } from './cacheableLocatorService';

Expand Down Expand Up @@ -190,15 +191,7 @@ export class PipEnvService extends CacheableLocatorService implements IPipEnvSer
} catch (error) {
const platformService = this.serviceContainer.get<IPlatformService>(IPlatformService);
const currentProc = this.serviceContainer.get<ICurrentProcess>(ICurrentProcess);
const enviromentVariableValues: Record<string, string | undefined> = {
LC_ALL: currentProc.env.LC_ALL,
LANG: currentProc.env.LANG
};
enviromentVariableValues[platformService.pathVariableName] =
currentProc.env[platformService.pathVariableName];

traceWarning('Error in invoking PipEnv', error);
traceWarning(`Relevant Environment Variables ${JSON.stringify(enviromentVariableValues, undefined, 4)}`);
traceProcessError(platformService, currentProc, error, 'PipEnv');
}
}
}
181 changes: 181 additions & 0 deletions src/client/interpreter/locators/services/poetryService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { inject, injectable } from 'inversify';
import * as path from 'path';
import { Uri } from 'vscode';
import { IApplicationShell, IWorkspaceService } from '../../../common/application/types';
import { traceError } from '../../../common/logger';
import { IFileSystem, IPlatformService } from '../../../common/platform/types';
import { IProcessServiceFactory } from '../../../common/process/types';
import { IConfigurationService, ICurrentProcess } from '../../../common/types';
import { Interpreters } from '../../../common/utils/localize';
import { StopWatch } from '../../../common/utils/stopWatch';
import { IServiceContainer } from '../../../ioc/types';
import { sendTelemetryEvent } from '../../../telemetry';
import { EventName } from '../../../telemetry/constants';
import {
GetInterpreterLocatorOptions,
IInterpreterHelper,
InterpreterType,
IPoetryService,
PythonInterpreter
} from '../../contracts';
import { traceProcessError } from '../helpers';
import { IPipEnvServiceHelper } from '../types';
import { CacheableLocatorService } from './cacheableLocatorService';

@injectable()
export class PoetryService extends CacheableLocatorService implements IPoetryService {
private readonly helper: IInterpreterHelper;
private readonly processServiceFactory: IProcessServiceFactory;
private readonly workspace: IWorkspaceService;
private readonly fs: IFileSystem;
private readonly configService: IConfigurationService;
private readonly poetryServiceHelper: IPipEnvServiceHelper;

constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
super('PoetryService', serviceContainer, true);
this.helper = this.serviceContainer.get<IInterpreterHelper>(IInterpreterHelper);
this.processServiceFactory = this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory);
this.workspace = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService);
this.fs = this.serviceContainer.get<IFileSystem>(IFileSystem);
this.configService = this.serviceContainer.get<IConfigurationService>(IConfigurationService);
this.poetryServiceHelper = this.serviceContainer.get<IPipEnvServiceHelper>(IPipEnvServiceHelper);
}

// tslint:disable-next-line:no-empty
public dispose() {}

public async isRelatedPoetryEnvironment(dir: string, pythonPath: string): Promise<boolean> {
if (!this.didTriggerInterpreterSuggestions) {
return false;
}

// In PipEnv, the name of the cwd is used as a prefix in the virtual env.
if (pythonPath.indexOf(`${path.sep}${path.basename(dir)}-`) === -1) {
return false;
}
const envName = await this.getInterpreterPathFromPoetry(dir, true);
return !!envName;
}

public get executable(): string {
return this.didTriggerInterpreterSuggestions ? this.configService.getSettings().poetryPath : '';
}

public async getInterpreters(resource?: Uri, options?: GetInterpreterLocatorOptions): Promise<PythonInterpreter[]> {
if (!this.didTriggerInterpreterSuggestions) {
return [];
}

const stopwatch = new StopWatch();
const startDiscoveryTime = stopwatch.elapsedTime;

const interpreters = await super.getInterpreters(resource, options);

const discoveryDuration = stopwatch.elapsedTime - startDiscoveryTime;
sendTelemetryEvent(EventName.POETRY_INTERPRETER_DISCOVERY, discoveryDuration);

return interpreters;
}

protected getInterpretersImplementation(resource?: Uri): Promise<PythonInterpreter[]> {
if (!this.didTriggerInterpreterSuggestions) {
return Promise.resolve([]);
}

const poetryCwd = this.getPoetryWorkingDirectory(resource);
if (!poetryCwd) {
return Promise.resolve([]);
}

return this.getInterpreterFromPoetry(poetryCwd)
.then((item) => (item ? [item] : []))
.catch(() => []);
}

private async getInterpreterFromPoetry(poetryCwd: string): Promise<PythonInterpreter | undefined> {
const interpreterPath = await this.getInterpreterPathFromPoetry(poetryCwd);
if (!interpreterPath) {
return;
}

const details = await this.helper.getInterpreterInformation(interpreterPath);
if (!details) {
return;
}
this._hasInterpreters.resolve(true);
await this.poetryServiceHelper.trackWorkspaceFolder(interpreterPath, Uri.file(poetryCwd));
return {
...(details as PythonInterpreter),
path: interpreterPath,
type: InterpreterType.Poetry,
poetryWorkspaceFolder: poetryCwd
};
}

private getPoetryWorkingDirectory(resource?: Uri): string | undefined {
// The file is not in a workspace. However, workspace may be opened
// and file is just a random file opened from elsewhere. In this case
// we still want to provide interpreter associated with the workspace.
// Otherwise if user tries and formats the file, we may end up using
// plain pip module installer to bring in the formatter and it is wrong.
const wsFolder = resource ? this.workspace.getWorkspaceFolder(resource) : undefined;
return wsFolder ? wsFolder.uri.fsPath : this.workspace.rootPath;
}

private async getInterpreterPathFromPoetry(cwd: string, ignoreErrors = false): Promise<string | undefined> {
// Quick check before actually running poetry
if (!(await this.checkIfPoetryFileExists(cwd))) {
return;
}
try {
// call poetry --help just to see if poetryWorkspaceFolder?: string; is in the PATH
const version = await this.invokePoetry(['--help'], cwd);
if (version === undefined) {
const appShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell);
appShell.showWarningMessage(Interpreters.poetryBinaryMissing().format(this.executable));
return;
}
// env info -p will be empty if a virtualenv has not been created yet
const venv = await this.invokePoetry(['env', 'info', '-p'], cwd);
if (venv === '') {
const appShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell);
appShell.showWarningMessage(Interpreters.poetryVenvMissing());
return;
}
const pythonPath = `${venv}/bin/python`;
return pythonPath && (await this.fs.fileExists(pythonPath)) ? pythonPath : undefined;
// tslint:disable-next-line:no-empty
} catch (error) {
traceError('Poetry identification failed', error);
if (ignoreErrors) {
return;
}
}
}

private async checkIfPoetryFileExists(cwd: string): Promise<boolean> {
if (await this.fs.fileExists(path.join(cwd, 'pyproject.toml'))) {
return true;
}
return false;
}

private async invokePoetry(arg: string[], rootPath: string): Promise<string | undefined> {
try {
const processService = await this.processServiceFactory.create(Uri.file(rootPath));
const execName = this.executable;
const result = await processService.exec(execName, arg, { cwd: rootPath });
if (result) {
return result.stdout ? result.stdout.trim() : '';
}
// tslint:disable-next-line:no-empty
} catch (error) {
const platformService = this.serviceContainer.get<IPlatformService>(IPlatformService);
const currentProc = this.serviceContainer.get<ICurrentProcess>(ICurrentProcess);
traceProcessError(platformService, currentProc, error, 'Poetry');
}
}
}
Loading