diff --git a/news/1 Enhancements/978.md b/news/1 Enhancements/978.md new file mode 100644 index 000000000000..544d85669309 --- /dev/null +++ b/news/1 Enhancements/978.md @@ -0,0 +1 @@ +Add the python.pipenvPath config setting. diff --git a/package.json b/package.json index 30c2095b5463..b384fb155399 100644 --- a/package.json +++ b/package.json @@ -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.", diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index b10c7ad0c686..61e12abbdd7c 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -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; @@ -137,6 +138,8 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { this.venvFolders = systemVariables.resolveAny(pythonSettings.get('venvFolders'))!; const condaPath = systemVariables.resolveAny(pythonSettings.get('condaPath'))!; this.condaPath = condaPath && condaPath.length > 0 ? getAbsolutePath(condaPath, workspaceRoot) : condaPath; + const pipenvPath = systemVariables.resolveAny(pythonSettings.get('pipenvPath'))!; + this.pipenvPath = pipenvPath && pipenvPath.length > 0 ? getAbsolutePath(pipenvPath, workspaceRoot) : pipenvPath; this.downloadLanguageServer = systemVariables.resolveAny(pythonSettings.get('downloadLanguageServer', true))!; this.jediEnabled = systemVariables.resolveAny(pythonSettings.get('jediEnabled', true))!; diff --git a/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts index b279c00de109..be61c16ccbda 100644 --- a/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts @@ -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; @@ -22,6 +25,7 @@ export class PipEnvActivationCommandProvider implements ITerminalActivationComma return; } - return ['pipenv shell']; + const execName = this.pipenvService.executable; + return [`${execName} shell`]; } } diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 3d8ad02b97a1..039a41e4d871 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -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; diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index f8db9ad82ba9..3ce2db2e75d7 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -113,6 +113,7 @@ export interface IInterpreterHelper { export const IPipEnvService = Symbol('IPipEnvService'); export interface IPipEnvService { + executable: string; isRelatedPipEnvironment(dir: string, pythonPath: string): Promise; } diff --git a/src/client/interpreter/locators/services/pipEnvService.ts b/src/client/interpreter/locators/services/pipEnvService.ts index fde8f6f69746..ede53aaaaf5c 100644 --- a/src/client/interpreter/locators/services/pipEnvService.ts +++ b/src/client/interpreter/locators/services/pipEnvService.ts @@ -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() @@ -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); @@ -31,6 +31,7 @@ export class PipEnvService extends CacheableLocatorService implements IPipEnvSer this.workspace = this.serviceContainer.get(IWorkspaceService); this.fs = this.serviceContainer.get(IFileSystem); this.logger = this.serviceContainer.get(ILogger); + this.configService = this.serviceContainer.get(IConfigurationService); } // tslint:disable-next-line:no-empty public dispose() { } @@ -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 { const pipenvCwd = this.getPipenvWorkingDirectory(resource); if (!pipenvCwd) { @@ -115,6 +121,7 @@ export class PipEnvService extends CacheableLocatorService implements IPipEnvSer private async invokePipenv(arg: string, rootPath: string): Promise { 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() : ''; diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index e571f73ed049..61a494d51a73 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -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(name)) .returns(() => sourceSettings[name]); } diff --git a/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts b/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts index de6ceba62116..4b42cfe9d1c1 100644 --- a/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts +++ b/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts @@ -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 @@ -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; setup(() => { interpreterService = mock(InterpreterService); - activationProvider = new PipEnvActivationCommandProvider(instance(interpreterService)); + pipenvService = TypeMoq.Mock.ofType(); + activationProvider = new PipEnvActivationCommandProvider( + instance(interpreterService), + pipenvService.object + ); + + pipenvService.setup(p => p.executable).returns(() => 'pipenv'); }); test('No commands for no interpreter', async () => { diff --git a/src/test/interpreters/pipEnvService.unit.test.ts b/src/test/interpreters/pipEnvService.unit.test.ts index 84d9069e05e5..70591d98fc2a 100644 --- a/src/test/interpreters/pipEnvService.unit.test.ts +++ b/src/test/interpreters/pipEnvService.unit.test.ts @@ -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'; @@ -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'; @@ -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; let interpreterHelper: TypeMoq.IMock; let processService: TypeMoq.IMock; @@ -42,6 +50,9 @@ suite('Interpreters - PipEnv', () => { let procServiceFactory: TypeMoq.IMock; let logger: TypeMoq.IMock; let platformService: TypeMoq.IMock; + let config: TypeMoq.IMock; + let settings: TypeMoq.IMock; + let pipenvPathSetting: string; setup(() => { serviceContainer = TypeMoq.Mock.ofType(); const workspaceService = TypeMoq.Mock.ofType(); @@ -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(); + settings = TypeMoq.Mock.ofType(); + 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); }); @@ -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'); + }); }); }); });