Skip to content

Commit

Permalink
Showing 4 changed files with 165 additions and 23 deletions.
15 changes: 10 additions & 5 deletions src/client/pythonEnvironments/creation/createEnvApi.ts
Original file line number Diff line number Diff line change
@@ -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)}`);
}
}),
);
52 changes: 34 additions & 18 deletions src/client/pythonEnvironments/creation/createEnvironment.ts
Original file line number Diff line number Diff line change
@@ -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 {
@@ -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) {
@@ -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<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;
@@ -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;
27 changes: 27 additions & 0 deletions src/client/pythonEnvironments/creation/types.ts
Original file line number Diff line number Diff line change
@@ -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<CreateEnvironmentResult | undefined>;
name: string;
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();
});
});
});

0 comments on commit 467823d

Please sign in to comment.