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

gh-978: Add python.pipenvPath config setting. #3957

1 change: 1 addition & 0 deletions news/1 Enhancements/978.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add the python.pipenvPath config setting.
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1521,6 +1521,12 @@
"description": "Path to the conda executable to use for activation (version 4.4+).",
"scope": "resource"
},
"python.pipenvPath": {
"type": "string",
"default": "pipenv",
"description": "Path to the pipenv executable to use for activation.",
"scope": "window"
},
"python.sortImports.args": {
"type": "array",
"description": "Arguments passed in. Each argument is a separate item in the array.",
Expand Down
3 changes: 3 additions & 0 deletions src/client/common/configSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings {
public venvPath = '';
public venvFolders: string[] = [];
public condaPath = '';
public pipenvPath = '';
public devOptions: string[] = [];
public linting!: ILintingSettings;
public formatting!: IFormattingSettings;
Expand Down Expand Up @@ -137,6 +138,8 @@ export class PythonSettings extends EventEmitter implements IPythonSettings {
this.venvFolders = systemVariables.resolveAny(pythonSettings.get<string[]>('venvFolders'))!;
const condaPath = systemVariables.resolveAny(pythonSettings.get<string>('condaPath'))!;
this.condaPath = condaPath && condaPath.length > 0 ? getAbsolutePath(condaPath, workspaceRoot) : condaPath;
const pipenvPath = systemVariables.resolveAny(pythonSettings.get<string>('pipenvPath'))!;
this.pipenvPath = pipenvPath && pipenvPath.length > 0 ? getAbsolutePath(pipenvPath, workspaceRoot) : pipenvPath;

this.downloadLanguageServer = systemVariables.resolveAny(pythonSettings.get<boolean>('downloadLanguageServer', true))!;
this.jediEnabled = systemVariables.resolveAny(pythonSettings.get<boolean>('jediEnabled', true))!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@

import { inject, injectable } from 'inversify';
import { Uri } from 'vscode';
import { IInterpreterService, InterpreterType } from '../../../interpreter/contracts';
import { IInterpreterService, InterpreterType, IPipEnvService } from '../../../interpreter/contracts';
import { ITerminalActivationCommandProvider, TerminalShellType } from '../types';

@injectable()
export class PipEnvActivationCommandProvider implements ITerminalActivationCommandProvider {
constructor(@inject(IInterpreterService) private readonly interpreterService: IInterpreterService) { }
constructor(
@inject(IInterpreterService) private readonly interpreterService: IInterpreterService,
@inject(IPipEnvService) private readonly pipenvService: IPipEnvService
) { }

public isShellSupported(_targetShell: TerminalShellType): boolean {
return true;
Expand All @@ -22,6 +25,7 @@ export class PipEnvActivationCommandProvider implements ITerminalActivationComma
return;
}

return ['pipenv shell'];
const execName = this.pipenvService.executable;
return [`${execName} shell`];
ericsnowcurrently marked this conversation as resolved.
Show resolved Hide resolved
}
}
1 change: 1 addition & 0 deletions src/client/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export interface IPythonSettings {
readonly venvPath: string;
readonly venvFolders: string[];
readonly condaPath: string;
readonly pipenvPath: string;
readonly downloadLanguageServer: boolean;
readonly jediEnabled: boolean;
readonly jediPath: string;
Expand Down
1 change: 1 addition & 0 deletions src/client/interpreter/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export interface IInterpreterHelper {

export const IPipEnvService = Symbol('IPipEnvService');
export interface IPipEnvService {
executable: string;
isRelatedPipEnvironment(dir: string, pythonPath: string): Promise<boolean>;
}

Expand Down
11 changes: 9 additions & 2 deletions src/client/interpreter/locators/services/pipEnvService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ import { IApplicationShell, IWorkspaceService } from '../../../common/applicatio
import { traceError } from '../../../common/logger';
import { IFileSystem, IPlatformService } from '../../../common/platform/types';
import { IProcessServiceFactory } from '../../../common/process/types';
import { ICurrentProcess, ILogger } from '../../../common/types';
import { IConfigurationService, ICurrentProcess, ILogger } from '../../../common/types';
import { IServiceContainer } from '../../../ioc/types';
import { IInterpreterHelper, InterpreterType, IPipEnvService, PythonInterpreter } from '../../contracts';
import { CacheableLocatorService } from './cacheableLocatorService';

const execName = 'pipenv';
const pipEnvFileNameVariable = 'PIPENV_PIPFILE';

@injectable()
Expand All @@ -23,6 +22,7 @@ export class PipEnvService extends CacheableLocatorService implements IPipEnvSer
private readonly workspace: IWorkspaceService;
private readonly fs: IFileSystem;
private readonly logger: ILogger;
private readonly configService: IConfigurationService;

constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
super('PipEnvService', serviceContainer);
Expand All @@ -31,6 +31,7 @@ export class PipEnvService extends CacheableLocatorService implements IPipEnvSer
this.workspace = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService);
this.fs = this.serviceContainer.get<IFileSystem>(IFileSystem);
this.logger = this.serviceContainer.get<ILogger>(ILogger);
this.configService = this.serviceContainer.get<IConfigurationService>(IConfigurationService);
}
// tslint:disable-next-line:no-empty
public dispose() { }
Expand All @@ -42,6 +43,11 @@ export class PipEnvService extends CacheableLocatorService implements IPipEnvSer
const envName = await this.getInterpreterPathFromPipenv(dir, true);
return !!envName;
}

public get executable(): string {
return this.configService.getSettings().pipenvPath;
}

protected getInterpretersImplementation(resource?: Uri): Promise<PythonInterpreter[]> {
const pipenvCwd = this.getPipenvWorkingDirectory(resource);
if (!pipenvCwd) {
Expand Down Expand Up @@ -115,6 +121,7 @@ export class PipEnvService extends CacheableLocatorService implements IPipEnvSer
private async invokePipenv(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) {
const stdout = result.stdout ? result.stdout.trim() : '';
Expand Down
2 changes: 1 addition & 1 deletion src/test/common/configSettings/configSettings.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ suite('Python Settings', () => {

function initializeConfig(sourceSettings: PythonSettings) {
// string settings
for (const name of ['pythonPath', 'venvPath', 'condaPath', 'envFile']) {
for (const name of ['pythonPath', 'venvPath', 'condaPath', 'pipenvPath', 'envFile']) {
config.setup(c => c.get<string>(name))
.returns(() => sourceSettings[name]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@

import * as assert from 'assert';
import { instance, mock, when } from 'ts-mockito';
import * as TypeMoq from 'typemoq';
import { Uri } from 'vscode';
import { PipEnvActivationCommandProvider } from '../../../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider';
import { ITerminalActivationCommandProvider, TerminalShellType } from '../../../../client/common/terminal/types';
import { getNamesAndValues } from '../../../../client/common/utils/enum';
import { IInterpreterService, InterpreterType } from '../../../../client/interpreter/contracts';
import { IInterpreterService, InterpreterType, IPipEnvService } from '../../../../client/interpreter/contracts';
import { InterpreterService } from '../../../../client/interpreter/interpreterService';

// tslint:disable:no-any
Expand All @@ -19,9 +20,16 @@ suite('Terminals Activation - Pipenv', () => {
suite(resource ? 'With a resource' : 'Without a resource', () => {
let activationProvider: ITerminalActivationCommandProvider;
let interpreterService: IInterpreterService;
let pipenvService: TypeMoq.IMock<IPipEnvService>;
setup(() => {
interpreterService = mock(InterpreterService);
activationProvider = new PipEnvActivationCommandProvider(instance(interpreterService));
pipenvService = TypeMoq.Mock.ofType<IPipEnvService>();
activationProvider = new PipEnvActivationCommandProvider(
instance(interpreterService),
pipenvService.object
);

pipenvService.setup(p => p.executable).returns(() => 'pipenv');
});

test('No commands for no interpreter', async () => {
Expand Down
29 changes: 26 additions & 3 deletions src/test/interpreters/pipEnvService.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

// tslint:disable:max-func-body-length no-any

import * as assert from 'assert';
import { expect } from 'chai';
import * as path from 'path';
import { SemVer } from 'semver';
Expand All @@ -13,10 +14,17 @@ import { Uri, WorkspaceFolder } from 'vscode';
import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types';
import { IFileSystem, IPlatformService } from '../../client/common/platform/types';
import { IProcessService, IProcessServiceFactory } from '../../client/common/process/types';
import { ICurrentProcess, ILogger, IPersistentState, IPersistentStateFactory } from '../../client/common/types';
import {
IConfigurationService,
ICurrentProcess,
ILogger,
IPersistentState,
IPersistentStateFactory,
IPythonSettings
} from '../../client/common/types';
import { getNamesAndValues } from '../../client/common/utils/enum';
import { IEnvironmentVariablesProvider } from '../../client/common/variables/types';
import { IInterpreterHelper, IInterpreterLocatorService } from '../../client/interpreter/contracts';
import { IInterpreterHelper } from '../../client/interpreter/contracts';
import { PipEnvService } from '../../client/interpreter/locators/services/pipEnvService';
import { IServiceContainer } from '../../client/ioc/types';

Expand All @@ -30,7 +38,7 @@ suite('Interpreters - PipEnv', () => {
[undefined, Uri.file(path.join(rootWorkspace, 'one.py'))].forEach(resource => {
const testSuffix = ` (${os.name}, ${resource ? 'with' : 'without'} a workspace)`;

let pipEnvService: IInterpreterLocatorService;
let pipEnvService: PipEnvService;
let serviceContainer: TypeMoq.IMock<IServiceContainer>;
let interpreterHelper: TypeMoq.IMock<IInterpreterHelper>;
let processService: TypeMoq.IMock<IProcessService>;
Expand All @@ -42,6 +50,9 @@ suite('Interpreters - PipEnv', () => {
let procServiceFactory: TypeMoq.IMock<IProcessServiceFactory>;
let logger: TypeMoq.IMock<ILogger>;
let platformService: TypeMoq.IMock<IPlatformService>;
let config: TypeMoq.IMock<IConfigurationService>;
let settings: TypeMoq.IMock<IPythonSettings>;
let pipenvPathSetting: string;
setup(() => {
serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>();
const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>();
Expand Down Expand Up @@ -80,6 +91,13 @@ suite('Interpreters - PipEnv', () => {
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider))).returns(() => envVarsProvider.object);
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ILogger))).returns(() => logger.object);
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformService.object);
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())).returns(() => config.object);

config = TypeMoq.Mock.ofType<IConfigurationService>();
settings = TypeMoq.Mock.ofType<IPythonSettings>();
config.setup(c => c.getSettings(TypeMoq.It.isValue(undefined))).returns(() => settings.object);
settings.setup(p => p.pipenvPath).returns(() => pipenvPathSetting);
pipenvPathSetting = 'pipenv';

pipEnvService = new PipEnvService(serviceContainer.object);
});
Expand Down Expand Up @@ -156,6 +174,11 @@ suite('Interpreters - PipEnv', () => {
expect(environments).to.be.lengthOf(1);
fileSystem.verifyAll();
});
test('Must use \'python.pipenvPath\' setting', async () => {
pipenvPathSetting = 'spam-spam-pipenv-spam-spam';
const pipenvExe = pipEnvService.executable;
assert.equal(pipenvExe, 'spam-spam-pipenv-spam-spam', 'Failed to identify pipenv.exe');
});
});
});
});