diff --git a/src/client/pythonEnvironments/creation/createEnvApi.ts b/src/client/pythonEnvironments/creation/createEnvApi.ts index b05670feae02..cfbf81909d59 100644 --- a/src/client/pythonEnvironments/creation/createEnvApi.ts +++ b/src/client/pythonEnvironments/creation/createEnvApi.ts @@ -9,7 +9,12 @@ import { IInterpreterQuickPick } from '../../interpreter/configuration/types'; import { getCreationEvents, handleCreateEnvironmentCommand } from './createEnvironment'; import { condaCreationProvider } from './provider/condaCreationProvider'; import { VenvCreationProvider } from './provider/venvCreationProvider'; -import { CreateEnvironmentOptions, CreateEnvironmentProvider, CreateEnvironmentResult } from './types'; +import { + CreateEnvironmentExitedEventArgs, + CreateEnvironmentOptions, + CreateEnvironmentProvider, + CreateEnvironmentResult, +} from './types'; import { showInformationMessage } from '../../common/vscodeApis/windowApis'; import { CreateEnv } from '../../common/utils/localize'; @@ -62,10 +67,10 @@ export function registerCreateEnvironmentFeatures( disposables.push(registerCreateEnvironmentProvider(new VenvCreationProvider(interpreterQuickPick))); disposables.push(registerCreateEnvironmentProvider(condaCreationProvider())); disposables.push( - onCreateEnvironmentExited(async (e: CreateEnvironmentResult | undefined) => { - if (e && e.path) { - await interpreterPathService.update(e.uri, ConfigurationTarget.WorkspaceFolder, e.path); - showInformationMessage(`${CreateEnv.informEnvCreation} ${pathUtils.getDisplayName(e.path)}`); + onCreateEnvironmentExited(async (e: CreateEnvironmentExitedEventArgs) => { + if (e.result?.path && e.options?.selectEnvironment) { + await interpreterPathService.update(e.result.uri, ConfigurationTarget.WorkspaceFolder, e.result.path); + showInformationMessage(`${CreateEnv.informEnvCreation} ${pathUtils.getDisplayName(e.result.path)}`); } }), ); diff --git a/src/client/pythonEnvironments/creation/createEnvironment.ts b/src/client/pythonEnvironments/creation/createEnvironment.ts index 2f21d787d336..bdeaf89ba82d 100644 --- a/src/client/pythonEnvironments/creation/createEnvironment.ts +++ b/src/client/pythonEnvironments/creation/createEnvironment.ts @@ -10,10 +10,16 @@ import { showQuickPickWithBack, } from '../../common/vscodeApis/windowApis'; import { traceError, traceVerbose } from '../../logging'; -import { CreateEnvironmentOptions, CreateEnvironmentProvider, CreateEnvironmentResult } from './types'; +import { + CreateEnvironmentExitedEventArgs, + CreateEnvironmentOptions, + CreateEnvironmentProvider, + CreateEnvironmentResult, + CreateEnvironmentStartedEventArgs, +} from './types'; -const onCreateEnvironmentStartedEvent = new EventEmitter(); -const onCreateEnvironmentExitedEvent = new EventEmitter(); +const onCreateEnvironmentStartedEvent = new EventEmitter(); +const onCreateEnvironmentExitedEvent = new EventEmitter(); let startedEventCount = 0; @@ -21,19 +27,19 @@ function isBusyCreatingEnvironment(): boolean { return startedEventCount > 0; } -function fireStartedEvent(): void { - onCreateEnvironmentStartedEvent.fire(); +function fireStartedEvent(options?: CreateEnvironmentOptions): void { + onCreateEnvironmentStartedEvent.fire({ options }); startedEventCount += 1; } -function fireExitedEvent(result: CreateEnvironmentResult | undefined): void { - onCreateEnvironmentExitedEvent.fire(result); +function fireExitedEvent(result?: CreateEnvironmentResult, options?: CreateEnvironmentOptions, error?: unknown): void { + onCreateEnvironmentExitedEvent.fire({ result, options, error }); startedEventCount -= 1; } export function getCreationEvents(): { - onCreateEnvironmentStarted: Event; - onCreateEnvironmentExited: Event; + onCreateEnvironmentStarted: Event; + onCreateEnvironmentExited: Event; isCreatingEnvironment: () => boolean; } { return { @@ -45,14 +51,12 @@ export function getCreationEvents(): { async function createEnvironment( provider: CreateEnvironmentProvider, - options: CreateEnvironmentOptions = { - ignoreSourceControl: true, - installPackages: true, - }, + options: CreateEnvironmentOptions, ): Promise { let result: CreateEnvironmentResult | undefined; + let err: unknown | undefined; try { - fireStartedEvent(); + fireStartedEvent(options); result = await provider.createEnvironment(options); } catch (ex) { if (ex === QuickInputButtons.Back) { @@ -61,9 +65,10 @@ async function createEnvironment( return undefined; } } - throw ex; + err = ex; + throw err; } finally { - fireExitedEvent(result); + fireExitedEvent(result, options, err); } return result; } @@ -110,17 +115,28 @@ async function showCreateEnvironmentQuickPick( return undefined; } +function getOptionsWithDefaults(options?: CreateEnvironmentOptions): CreateEnvironmentOptions { + return { + installPackages: true, + ignoreSourceControl: true, + showBackButton: false, + selectEnvironment: true, + ...options, + }; +} + export async function handleCreateEnvironmentCommand( providers: readonly CreateEnvironmentProvider[], options?: CreateEnvironmentOptions, ): Promise { + const optionsWithDefaults = getOptionsWithDefaults(options); let selectedProvider: CreateEnvironmentProvider | undefined; const envTypeStep = new MultiStepNode( undefined, async (context?: MultiStepAction) => { if (providers.length > 0) { try { - selectedProvider = await showCreateEnvironmentQuickPick(providers, options); + selectedProvider = await showCreateEnvironmentQuickPick(providers, optionsWithDefaults); } catch (ex) { if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { return ex; @@ -152,7 +168,7 @@ export async function handleCreateEnvironmentCommand( } if (selectedProvider) { try { - result = await createEnvironment(selectedProvider, options); + result = await createEnvironment(selectedProvider, optionsWithDefaults); } catch (ex) { if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { return ex; diff --git a/src/client/pythonEnvironments/creation/types.ts b/src/client/pythonEnvironments/creation/types.ts index c18249a2bd72..6dbd8adfe1f4 100644 --- a/src/client/pythonEnvironments/creation/types.ts +++ b/src/client/pythonEnvironments/creation/types.ts @@ -6,9 +6,26 @@ import { Progress, Uri } from 'vscode'; export interface CreateEnvironmentProgress extends Progress<{ message?: string; increment?: number }> {} export interface CreateEnvironmentOptions { + /** + * Default `true`. If `true`, the environment creation handler is expected to install packages. + */ installPackages?: boolean; + + /** + * Default `true`. If `true`, the environment creation provider is expected to add the environment to ignore list + * for the source control. + */ ignoreSourceControl?: boolean; + + /** + * Default `false`. If `true` the creation provider should show back button when showing QuickPick or QuickInput. + */ showBackButton?: boolean; + + /** + * Default `true`. If `true`, the environment will be selected as the environment to be used for the workspace. + */ + selectEnvironment?: boolean; } export interface CreateEnvironmentResult { @@ -17,6 +34,16 @@ export interface CreateEnvironmentResult { action?: 'Back' | 'Cancel'; } +export interface CreateEnvironmentStartedEventArgs { + options: CreateEnvironmentOptions | undefined; +} + +export interface CreateEnvironmentExitedEventArgs { + result: CreateEnvironmentResult | undefined; + error?: unknown; + options: CreateEnvironmentOptions | undefined; +} + export interface CreateEnvironmentProvider { createEnvironment(options?: CreateEnvironmentOptions): Promise; name: string; diff --git a/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts new file mode 100644 index 000000000000..1286ac44d58d --- /dev/null +++ b/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { IDisposableRegistry, IInterpreterPathService, IPathUtils } from '../../../client/common/types'; +import * as commandApis from '../../../client/common/vscodeApis/commandApis'; +import { IInterpreterQuickPick } from '../../../client/interpreter/configuration/types'; +import { registerCreateEnvironmentFeatures } from '../../../client/pythonEnvironments/creation/createEnvApi'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import { CreateEnvironmentProvider } from '../../../client/pythonEnvironments/creation/types'; +import { handleCreateEnvironmentCommand } from '../../../client/pythonEnvironments/creation/createEnvironment'; + +chaiUse(chaiAsPromised); + +suite('Create Environment APIs', () => { + let registerCommandStub: sinon.SinonStub; + let showQuickPickStub: sinon.SinonStub; + let showInformationMessageStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let interpreterQuickPick: typemoq.IMock; + let interpreterPathService: typemoq.IMock; + let pathUtils: typemoq.IMock; + + setup(() => { + showQuickPickStub = sinon.stub(windowApis, 'showQuickPick'); + showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); + + registerCommandStub = sinon.stub(commandApis, 'registerCommand'); + interpreterQuickPick = typemoq.Mock.ofType(); + interpreterPathService = typemoq.Mock.ofType(); + pathUtils = typemoq.Mock.ofType(); + + registerCommandStub.callsFake((_command: string, _callback: (...args: any[]) => any) => ({ + dispose: () => { + // Do nothing + }, + })); + + pathUtils.setup((p) => p.getDisplayName(typemoq.It.isAny())).returns(() => 'test'); + + registerCreateEnvironmentFeatures( + disposables, + interpreterQuickPick.object, + interpreterPathService.object, + pathUtils.object, + ); + }); + teardown(() => { + disposables.forEach((d) => d.dispose()); + sinon.restore(); + }); + + [true, false].forEach((selectEnvironment) => { + test(`Set environment selectEnvironment == ${selectEnvironment}`, async () => { + const provider = typemoq.Mock.ofType(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider + .setup((p) => p.createEnvironment(typemoq.It.isAny())) + .returns(() => + Promise.resolve({ + path: '/path/to/env', + uri: Uri.file('/path/to/env'), + }), + ); + provider.setup((p) => (p as any).then).returns(() => undefined); + + showQuickPickStub.resolves(provider.object); + + interpreterPathService + .setup((p) => + p.update( + typemoq.It.isAny(), + ConfigurationTarget.WorkspaceFolder, + typemoq.It.isValue('/path/to/env'), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(selectEnvironment ? typemoq.Times.once() : typemoq.Times.never()); + + await handleCreateEnvironmentCommand([provider.object], { selectEnvironment }); + + assert.ok(showQuickPickStub.calledOnce); + assert.ok(selectEnvironment ? showInformationMessageStub.calledOnce : showInformationMessageStub.notCalled); + interpreterPathService.verifyAll(); + }); + }); +});