Skip to content
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
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`];
Comment thread
ericsnowcurrently marked this conversation as 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');
});
});
});
});