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

Create environment proposed API #21074

Merged
merged 3 commits into from
Apr 19, 2023
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
2 changes: 2 additions & 0 deletions src/client/environmentApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
ResolvedEnvironment,
Resource,
} from './apiTypes';
import { buildEnvironmentCreationApi } from './pythonEnvironments/creation/createEnvApi';

type ActiveEnvironmentChangeEvent = {
resource: WorkspaceFolder | undefined;
Expand Down Expand Up @@ -253,6 +254,7 @@ export function buildEnvironmentApi(
sendApiTelemetry('onDidChangeEnvironments');
return onEnvironmentsChanged.event;
},
...buildEnvironmentCreationApi(),
karthiknadig marked this conversation as resolved.
Show resolved Hide resolved
};
return environmentApi;
}
Expand Down
6 changes: 5 additions & 1 deletion src/client/proposedApiTypes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

export interface ProposedExtensionAPI {}
export interface ProposedExtensionAPI {
karthiknadig marked this conversation as resolved.
Show resolved Hide resolved
/**
* Top level proposed APIs should go here.
*/
}
51 changes: 38 additions & 13 deletions src/client/pythonEnvironments/creation/createEnvApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import { IInterpreterQuickPick } from '../../interpreter/configuration/types';
import { getCreationEvents, handleCreateEnvironmentCommand } from './createEnvironment';
import { condaCreationProvider } from './provider/condaCreationProvider';
import { VenvCreationProvider } from './provider/venvCreationProvider';
import { showInformationMessage } from '../../common/vscodeApis/windowApis';
import { CreateEnv } from '../../common/utils/localize';
import {
CreateEnvironmentExitedEventArgs,
CreateEnvironmentOptions,
CreateEnvironmentProvider,
CreateEnvironmentOptions,
CreateEnvironmentResult,
} from './types';
import { showInformationMessage } from '../../common/vscodeApis/windowApis';
import { CreateEnv } from '../../common/utils/localize';
ProposedCreateEnvironmentAPI,
EnvironmentDidCreateEvent,
} from './proposed.createEnvApis';

class CreateEnvironmentProviders {
private _createEnvProviders: CreateEnvironmentProvider[] = [];
Expand All @@ -26,6 +27,9 @@ class CreateEnvironmentProviders {
}

public add(provider: CreateEnvironmentProvider) {
if (this._createEnvProviders.filter((p) => p.id === provider.id).length > 0) {
throw new Error(`Create Environment provider with id ${provider.id} already registered`);
}
this._createEnvProviders.push(provider);
}

Expand Down Expand Up @@ -63,15 +67,36 @@ export function registerCreateEnvironmentFeatures(
return handleCreateEnvironmentCommand(providers, options);
},
),
);
disposables.push(registerCreateEnvironmentProvider(new VenvCreationProvider(interpreterQuickPick)));
disposables.push(registerCreateEnvironmentProvider(condaCreationProvider()));
disposables.push(
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)}`);
registerCreateEnvironmentProvider(new VenvCreationProvider(interpreterQuickPick)),
registerCreateEnvironmentProvider(condaCreationProvider()),
onCreateEnvironmentExited(async (e: EnvironmentDidCreateEvent) => {
if (e.path && e.options?.selectEnvironment) {
await interpreterPathService.update(
e.workspaceFolder?.uri,
ConfigurationTarget.WorkspaceFolder,
e.path,
);
showInformationMessage(`${CreateEnv.informEnvCreation} ${pathUtils.getDisplayName(e.path)}`);
}
}),
);
}

export function buildEnvironmentCreationApi(): ProposedCreateEnvironmentAPI {
return {
onWillCreateEnvironment: onCreateEnvironmentStarted,
onDidCreateEnvironment: onCreateEnvironmentExited,
createEnvironment: async (
options?: CreateEnvironmentOptions | undefined,
): Promise<CreateEnvironmentResult | undefined> => {
const providers = _createEnvironmentProviders.getAll();
try {
return await handleCreateEnvironmentCommand(providers, options);
} catch (err) {
return { path: undefined, workspaceFolder: undefined, action: undefined, error: err as Error };
}
},
registerCreateEnvironmentProvider: (provider: CreateEnvironmentProvider) =>
registerCreateEnvironmentProvider(provider),
};
}
36 changes: 19 additions & 17 deletions src/client/pythonEnvironments/creation/createEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import {
} from '../../common/vscodeApis/windowApis';
import { traceError, traceVerbose } from '../../logging';
import {
CreateEnvironmentExitedEventArgs,
CreateEnvironmentOptions,
CreateEnvironmentProvider,
CreateEnvironmentResult,
CreateEnvironmentStartedEventArgs,
} from './types';
CreateEnvironmentProvider,
EnvironmentWillCreateEvent,
EnvironmentDidCreateEvent,
} from './proposed.createEnvApis';

const onCreateEnvironmentStartedEvent = new EventEmitter<CreateEnvironmentStartedEventArgs>();
const onCreateEnvironmentExitedEvent = new EventEmitter<CreateEnvironmentExitedEventArgs>();
const onCreateEnvironmentStartedEvent = new EventEmitter<EnvironmentWillCreateEvent>();
const onCreateEnvironmentExitedEvent = new EventEmitter<EnvironmentDidCreateEvent>();

let startedEventCount = 0;

Expand All @@ -32,14 +32,20 @@ function fireStartedEvent(options?: CreateEnvironmentOptions): void {
startedEventCount += 1;
}

function fireExitedEvent(result?: CreateEnvironmentResult, options?: CreateEnvironmentOptions, error?: unknown): void {
onCreateEnvironmentExitedEvent.fire({ result, options, error });
function fireExitedEvent(result?: CreateEnvironmentResult, options?: CreateEnvironmentOptions, error?: Error): void {
onCreateEnvironmentExitedEvent.fire({
options,
workspaceFolder: result?.workspaceFolder,
path: result?.path,
action: result?.action,
error: error || result?.error,
});
startedEventCount -= 1;
}

export function getCreationEvents(): {
onCreateEnvironmentStarted: Event<CreateEnvironmentStartedEventArgs>;
onCreateEnvironmentExited: Event<CreateEnvironmentExitedEventArgs>;
onCreateEnvironmentStarted: Event<EnvironmentWillCreateEvent>;
onCreateEnvironmentExited: Event<EnvironmentDidCreateEvent>;
isCreatingEnvironment: () => boolean;
} {
return {
Expand All @@ -54,7 +60,7 @@ async function createEnvironment(
options: CreateEnvironmentOptions,
): Promise<CreateEnvironmentResult | undefined> {
let result: CreateEnvironmentResult | undefined;
let err: unknown | undefined;
let err: Error | undefined;
try {
fireStartedEvent(options);
result = await provider.createEnvironment(options);
Expand All @@ -65,7 +71,7 @@ async function createEnvironment(
return undefined;
}
}
err = ex;
err = ex as Error;
throw err;
} finally {
fireExitedEvent(result, options, err);
Expand Down Expand Up @@ -185,11 +191,7 @@ export async function handleCreateEnvironmentCommand(
const action = await MultiStepNode.run(envTypeStep);
if (options?.showBackButton) {
if (action === MultiStepAction.Back || action === MultiStepAction.Cancel) {
result = {
path: result?.path,
uri: result?.uri,
action: action === MultiStepAction.Back ? 'Back' : 'Cancel',
};
result = { action, workspaceFolder: undefined, path: undefined, error: undefined };
}
}

Expand Down
143 changes: 143 additions & 0 deletions src/client/pythonEnvironments/creation/proposed.createEnvApis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License

import { Event, Disposable, WorkspaceFolder } from 'vscode';
import { EnvironmentTools } from '../../apiTypes';

export type CreateEnvironmentUserActions = 'Back' | 'Cancel';
export type EnvironmentProviderId = string;

/**
* Options used when creating a Python environment.
*/
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;
karthiknadig marked this conversation as resolved.
Show resolved Hide resolved

/**
* Default `true`. If `true`, the environment after creation will be selected.
*/
selectEnvironment?: boolean;
}

/**
* Params passed on `onWillCreateEnvironment` event handler.
*/
export interface EnvironmentWillCreateEvent {
/**
* Options used to create a Python environment.
*/
options: CreateEnvironmentOptions | undefined;
}

/**
* Params passed on `onDidCreateEnvironment` event handler.
*/
export interface EnvironmentDidCreateEvent extends CreateEnvironmentResult {
/**
* Options used to create the Python environment.
*/
options: CreateEnvironmentOptions | undefined;
}

export interface CreateEnvironmentResult {
/**
* Workspace folder associated with the environment.
*/
workspaceFolder: WorkspaceFolder | undefined;

/**
* Path to the executable python in the environment
*/
path: string | undefined;

/**
* User action that resulted in exit from the create environment flow.
*/
action: CreateEnvironmentUserActions | undefined;

/**
* Error if any occurred during environment creation.
*/
error: Error | undefined;
}

/**
* Extensions that want to contribute their own environment creation can do that by registering an object
* that implements this interface.
*/
export interface CreateEnvironmentProvider {
/**
* This API is called when user selects this provider from a QuickPick to select the type of environment
* user wants. This API is expected to show a QuickPick or QuickInput to get the user input and return
* the path to the Python executable in the environment.
*
* @param {CreateEnvironmentOptions} [options] Options used to create a Python environment.
*
* @returns a promise that resolves to the path to the
* Python executable in the environment. Or any action taken by the user, such as back or cancel.
*/
createEnvironment(options?: CreateEnvironmentOptions): Promise<CreateEnvironmentResult | undefined>;

/**
* Unique ID for the creation provider, typically <ExtensionId>:<environment-type | guid>
*/
id: EnvironmentProviderId;

/**
* Display name for the creation provider.
*/
name: string;

/**
* Description displayed to the user in the QuickPick to select environment provider.
*/
description: string;

/**
* Tools used to manage this environment. e.g., ['conda']. In the most to least priority order
* for resolving and working with the environment.
*/
tools: EnvironmentTools[];
}

export interface ProposedCreateEnvironmentAPI {
/**
* This API can be used to detect when the environment creation starts for any registered
* provider (including internal providers). This will also receive any options passed in
* or defaults used to create environment.
*/
onWillCreateEnvironment: Event<EnvironmentWillCreateEvent>;

/**
* This API can be used to detect when the environment provider exits for any registered
* provider (including internal providers). This will also receive created environment path,
* any errors, or user actions taken from the provider.
*/
onDidCreateEnvironment: Event<EnvironmentDidCreateEvent>;

/**
* This API will show a QuickPick to select an environment provider from available list of
* providers. Based on the selection the `createEnvironment` will be called on the provider.
*/
createEnvironment(options?: CreateEnvironmentOptions): Promise<CreateEnvironmentResult | undefined>;

/**
* This API should be called to register an environment creation provider. It returns
* a (@link Disposable} which can be used to remove the registration.
*/
registerCreateEnvironmentProvider(provider: CreateEnvironmentProvider): Disposable;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@ import { CancellationToken, ProgressLocation, WorkspaceFolder } from 'vscode';
import * as path from 'path';
import { Commands, PVSC_EXTENSION_ID } from '../../../common/constants';
import { traceError, traceLog } from '../../../logging';
import {
CreateEnvironmentOptions,
CreateEnvironmentProgress,
CreateEnvironmentProvider,
CreateEnvironmentResult,
} from '../types';
import { CreateEnvironmentProgress } from '../types';
import { pickWorkspaceFolder } from '../common/workspaceSelection';
import { execObservable } from '../../../common/process/rawProcessApis';
import { createDeferred } from '../../../common/utils/async';
Expand All @@ -28,6 +23,11 @@ import {
CONDA_ENV_EXISTING_MARKER,
} from './condaProgressAndTelemetry';
import { splitLines } from '../../../common/stringUtils';
import {
CreateEnvironmentOptions,
CreateEnvironmentResult,
CreateEnvironmentProvider,
} from '../proposed.createEnvApis';

function generateCommandArgs(version?: string, options?: CreateEnvironmentOptions): string[] {
let addGitIgnore = true;
Expand Down Expand Up @@ -247,7 +247,7 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise<Cr
showErrorMessageWithLogs(CreateEnv.Conda.errorCreatingEnvironment);
}
}
return { path: envPath, uri: workspace?.uri };
return { path: envPath, workspaceFolder: workspace, action: undefined, error: undefined };
},
);
}
Expand All @@ -260,5 +260,7 @@ export function condaCreationProvider(): CreateEnvironmentProvider {
description: CreateEnv.Conda.providerDescription,

id: `${PVSC_EXTENSION_ID}:conda`,

tools: ['Conda'],
};
}
Loading