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

Add option to control if environment is selected after creation #20738

Merged
merged 3 commits into from
Mar 6, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
15 changes: 10 additions & 5 deletions src/client/pythonEnvironments/creation/createEnvApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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)}`);
}
}),
);
Expand Down
52 changes: 34 additions & 18 deletions src/client/pythonEnvironments/creation/createEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,36 @@ 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<void>();
const onCreateEnvironmentExitedEvent = new EventEmitter<CreateEnvironmentResult | undefined>();
const onCreateEnvironmentStartedEvent = new EventEmitter<CreateEnvironmentStartedEventArgs>();
const onCreateEnvironmentExitedEvent = new EventEmitter<CreateEnvironmentExitedEventArgs>();

let startedEventCount = 0;

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<void>;
onCreateEnvironmentExited: Event<CreateEnvironmentResult | undefined>;
onCreateEnvironmentStarted: Event<CreateEnvironmentStartedEventArgs>;
onCreateEnvironmentExited: Event<CreateEnvironmentExitedEventArgs>;
isCreatingEnvironment: () => boolean;
} {
return {
Expand All @@ -45,14 +51,12 @@ export function getCreationEvents(): {

async function createEnvironment(
provider: CreateEnvironmentProvider,
options: CreateEnvironmentOptions = {
ignoreSourceControl: true,
installPackages: true,
},
options: CreateEnvironmentOptions,
): Promise<CreateEnvironmentResult | undefined> {
let result: CreateEnvironmentResult | undefined;
let err: unknown | undefined;
try {
fireStartedEvent();
fireStartedEvent(options);
result = await provider.createEnvironment(options);
} catch (ex) {
if (ex === QuickInputButtons.Back) {
Expand All @@ -61,9 +65,10 @@ async function createEnvironment(
return undefined;
}
}
throw ex;
err = ex;
throw err;
} finally {
fireExitedEvent(result);
fireExitedEvent(result, options, err);
}
return result;
}
Expand Down Expand Up @@ -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<CreateEnvironmentResult | undefined> {
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;
Expand Down Expand Up @@ -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;
Expand Down
19 changes: 19 additions & 0 deletions src/client/pythonEnvironments/creation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,18 @@ 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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, /** ... **/ docstrings are interpreted by Intellisense and hence preferred.

selectEnvironment?: boolean;
}

export interface CreateEnvironmentResult {
Expand All @@ -17,6 +26,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<CreateEnvironmentResult | undefined>;
name: string;
Expand Down
94 changes: 94 additions & 0 deletions src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts
Original file line number Diff line number Diff line change
@@ -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<IInterpreterQuickPick>;
let interpreterPathService: typemoq.IMock<IInterpreterPathService>;
let pathUtils: typemoq.IMock<IPathUtils>;

setup(() => {
showQuickPickStub = sinon.stub(windowApis, 'showQuickPick');
showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage');

registerCommandStub = sinon.stub(commandApis, 'registerCommand');
interpreterQuickPick = typemoq.Mock.ofType<IInterpreterQuickPick>();
interpreterPathService = typemoq.Mock.ofType<IInterpreterPathService>();
pathUtils = typemoq.Mock.ofType<IPathUtils>();

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<CreateEnvironmentProvider>();
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();
});
});
});