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

Show a prompt asking users if they want to create environment #22071

Merged
merged 4 commits into from
Sep 25, 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
13 changes: 13 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,19 @@
"experimental"
]
},
"python.createEnvironment.trigger": {
"default": "off",
"markdownDescription": "%python.createEnvironment.trigger.description%",
"scope": "machine-overridable",
"type": "string",
"enum": [
"off",
"prompt"
],
"tags": [
"experimental"
]
},
"python.condaPath": {
"default": "",
"description": "%python.condaPath.description%",
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"python.command.python.launchTensorBoard.title": "Launch TensorBoard",
"python.command.python.refreshTensorBoard.title": "Refresh TensorBoard",
"python.createEnvironment.contentButton.description": "Show or hide Create Environment button in the editor for `requirements.txt` or other dependency files.",
"python.createEnvironment.trigger.description": "Detect if environment creation is required for the current project",
"python.menu.createNewFile.title": "Python File",
"python.editor.context.submenu.runPython": "Run Python",
"python.editor.context.submenu.runPythonInteractive": "Run in Interactive window",
Expand Down
1 change: 1 addition & 0 deletions src/client/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export namespace Commands {
export const ClearWorkspaceInterpreter = 'python.clearWorkspaceInterpreter';
export const Create_Environment = 'python.createEnvironment';
export const Create_Environment_Button = 'python.createEnvironment-button';
export const Create_Environment_Check = 'python.createEnvironmentCheck';
export const Create_Terminal = 'python.createTerminal';
export const Debug_In_Terminal = 'python.debugInTerminal';
export const Enable_SourceMap_Support = 'python.enableSourceMapSupport';
Expand Down
45 changes: 44 additions & 1 deletion src/client/common/persistentState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,46 @@ import {
import { cache } from './utils/decorators';
import { noop } from './utils/misc';

let _workspaceState: Memento | undefined;
const _workspaceKeys: string[] = [];
export function initializePersistentStateForTriggers(context: IExtensionContext) {
_workspaceState = context.workspaceState;
}

export function getWorkspaceStateValue<T>(key: string, defaultValue?: T): T | undefined {
if (!_workspaceState) {
throw new Error('Workspace state not initialized');
}
if (defaultValue === undefined) {
return _workspaceState.get<T>(key);
}
return _workspaceState.get<T>(key, defaultValue);
}

export async function updateWorkspaceStateValue<T>(key: string, value: T): Promise<void> {
if (!_workspaceState) {
throw new Error('Workspace state not initialized');
}
try {
_workspaceKeys.push(key);
await _workspaceState.update(key, value);
const after = getWorkspaceStateValue(key);
if (JSON.stringify(after) !== JSON.stringify(value)) {
await _workspaceState.update(key, undefined);
await _workspaceState.update(key, value);
traceError('Error while updating workspace state for key:', key);
}
} catch (ex) {
traceError(`Error while updating workspace state for key [${key}]:`, ex);
}
}
Comment on lines +25 to +55

Choose a reason for hiding this comment

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

Is it possible for you to use PersistentState class itself? That way logic isn't replicated, and it also has the "expiry" feature.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added a task to do this as a part of the Testing week. #22077


async function clearWorkspaceState(): Promise<void> {
if (_workspaceState !== undefined) {
await Promise.all(_workspaceKeys.map((key) => updateWorkspaceStateValue(key, undefined)));
}
}

export class PersistentState<T> implements IPersistentState<T> {
constructor(
public readonly storage: Memento,
Expand Down Expand Up @@ -93,7 +133,10 @@ export class PersistentStateFactory implements IPersistentStateFactory, IExtensi
) {}

public async activate(): Promise<void> {
this.cmdManager?.registerCommand(Commands.ClearStorage, this.cleanAllPersistentStates.bind(this));
this.cmdManager?.registerCommand(Commands.ClearStorage, async () => {
await clearWorkspaceState();
await this.cleanAllPersistentStates();
});
const globalKeysStorageDeprecated = this.createGlobalPersistentState(GLOBAL_PERSISTENT_KEYS_DEPRECATED, []);
const workspaceKeysStorageDeprecated = this.createWorkspacePersistentState(
WORKSPACE_PERSISTENT_KEYS_DEPRECATED,
Expand Down
9 changes: 9 additions & 0 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,15 @@ export namespace CreateEnv {
export const deletingEnvironmentProgress = l10n.t('Deleting existing ".conda" environment...');
export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".conda" environment.');
}

export namespace Trigger {
export const workspaceTriggerMessage = l10n.t(
'A virtual environment is not currently selected for your Python interpreter. Would you like to create a virtual environment?',
);
export const createEnvironment = l10n.t('Create');
export const disableCheck = l10n.t('Disable');
export const disableCheckWorkspace = l10n.t('Disable (Workspace)');
}
}

export namespace ToolsExtensions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import { DebuggerTypeName } from '../../../constants';
import { DebugOptions, DebugPurpose, LaunchRequestArguments } from '../../../types';
import { BaseConfigurationResolver } from './base';
import { getProgram, IDebugEnvironmentVariablesService } from './helper';
import {
CreateEnvironmentCheckKind,
triggerCreateEnvironmentCheckNonBlocking,
} from '../../../../pythonEnvironments/creation/createEnvironmentTrigger';
import { sendTelemetryEvent } from '../../../../telemetry';
import { EventName } from '../../../../telemetry/constants';

@injectable()
export class LaunchConfigurationResolver extends BaseConfigurationResolver<LaunchRequestArguments> {
Expand Down Expand Up @@ -84,6 +90,8 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver<Launc
(item, pos) => debugConfiguration.debugOptions!.indexOf(item) === pos,
);
}
sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'debug' });
triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.Workspace, workspaceFolder);
return debugConfiguration;
}

Expand Down
6 changes: 6 additions & 0 deletions src/client/debugger/extension/debugCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import { DebugPurpose, LaunchRequestArguments } from '../types';
import { IInterpreterService } from '../../interpreter/contracts';
import { noop } from '../../common/utils/misc';
import { getConfigurationsByUri } from './configuration/launch.json/launchJsonReader';
import {
CreateEnvironmentCheckKind,
triggerCreateEnvironmentCheckNonBlocking,
} from '../../pythonEnvironments/creation/createEnvironmentTrigger';

@injectable()
export class DebugCommands implements IExtensionSingleActivationService {
Expand All @@ -35,6 +39,8 @@ export class DebugCommands implements IExtensionSingleActivationService {
this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop);
return;
}
sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'debug-in-terminal' });
triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file);
const config = await DebugCommands.getDebugConfiguration(file);
this.debugService.startDebugging(undefined, config);
}),
Expand Down
4 changes: 4 additions & 0 deletions src/client/extensionActivation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ import { DynamicPythonDebugConfigurationService } from './debugger/extension/con
import { IInterpreterQuickPick } from './interpreter/configuration/types';
import { registerInstallFormatterPrompt } from './providers/prompts/installFormatterPrompt';
import { registerAllCreateEnvironmentFeatures } from './pythonEnvironments/creation/registrations';
import { registerCreateEnvironmentTriggers } from './pythonEnvironments/creation/createEnvironmentTrigger';
import { initializePersistentStateForTriggers } from './common/persistentState';

export async function activateComponents(
// `ext` is passed to any extra activation funcs.
Expand Down Expand Up @@ -199,6 +201,8 @@ async function activateLegacy(ext: ExtensionState): Promise<ActivationResult> {
);

registerInstallFormatterPrompt(serviceContainer);
registerCreateEnvironmentTriggers(disposables);
initializePersistentStateForTriggers(ext.context);
}
}

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

import * as path from 'path';
import * as fsapi from 'fs-extra';
import { ConfigurationTarget, Uri, WorkspaceFolder } from 'vscode';
import { getPipRequirementsFiles } from '../provider/venvUtils';
import { getExtension } from '../../../common/vscodeApis/extensionsApi';
import { PVSC_EXTENSION_ID } from '../../../common/constants';
import { PythonExtension } from '../../../api/types';
import { traceVerbose } from '../../../logging';
import { getConfiguration } from '../../../common/vscodeApis/workspaceApis';
import { getWorkspaceStateValue, updateWorkspaceStateValue } from '../../../common/persistentState';

export const CREATE_ENV_TRIGGER_SETTING_PART = 'createEnvironment.trigger';
export const CREATE_ENV_TRIGGER_SETTING = `python.${CREATE_ENV_TRIGGER_SETTING_PART}`;

export async function fileContainsInlineDependencies(_uri: Uri): Promise<boolean> {
// This is a placeholder for the real implementation of inline dependencies support
// For now we don't detect anything. Once PEP-722/PEP-723 are accepted we can implement
// this properly.
return false;
}

export async function hasRequirementFiles(workspace: WorkspaceFolder): Promise<boolean> {
const files = await getPipRequirementsFiles(workspace);
const found = (files?.length ?? 0) > 0;
if (found) {
traceVerbose(`Found requirement files: ${workspace.uri.fsPath}`);
}
return found;
}

export async function hasKnownFiles(workspace: WorkspaceFolder): Promise<boolean> {
const filePaths: string[] = [
'poetry.lock',
'conda.yaml',
'environment.yaml',
'conda.yml',
'environment.yml',
'Pipfile',
'Pipfile.lock',
].map((fileName) => path.join(workspace.uri.fsPath, fileName));
const result = await Promise.all(filePaths.map((f) => fsapi.pathExists(f)));
const found = result.some((r) => r);
if (found) {
traceVerbose(`Found known files: ${workspace.uri.fsPath}`);
}
return found;
}

export async function isGlobalPythonSelected(workspace: WorkspaceFolder): Promise<boolean> {
const extension = getExtension<PythonExtension>(PVSC_EXTENSION_ID);
if (!extension) {
return false;
}
const extensionApi: PythonExtension = extension.exports as PythonExtension;
const interpreter = extensionApi.environments.getActiveEnvironmentPath(workspace.uri);
const details = await extensionApi.environments.resolveEnvironment(interpreter);
const isGlobal = details?.environment === undefined;
if (isGlobal) {
traceVerbose(`Selected python for [${workspace.uri.fsPath}] is [global] type: ${interpreter.path}`);
}
return isGlobal;
}

/**
* Checks the setting `python.createEnvironment.trigger` to see if we should perform the checks
* to prompt to create an environment.
* @export
* @returns : True if we should prompt to create an environment.
*/
export function shouldPromptToCreateEnv(): boolean {
const config = getConfiguration('python');
if (config) {
const value = config.get<string>(CREATE_ENV_TRIGGER_SETTING_PART, 'off');
return value !== 'off';
}

return getWorkspaceStateValue<string>(CREATE_ENV_TRIGGER_SETTING, 'off') !== 'off';
}

/**
* Sets `python.createEnvironment.trigger` to 'off' in the user settings.
*/
export function disableCreateEnvironmentTrigger(): void {
const config = getConfiguration('python');
if (config) {
config.update('createEnvironment.trigger', 'off', ConfigurationTarget.Global);
}
}

/**
* Sets trigger to 'off' in workspace persistent state. This disables trigger check
* for the current workspace only. In multi root case, it is disabled for all folders
* in the multi root workspace.
*/
export async function disableWorkspaceCreateEnvironmentTrigger(): Promise<void> {
await updateWorkspaceStateValue(CREATE_ENV_TRIGGER_SETTING, 'off');
}

let _alreadyCreateEnvCriteriaCheck = false;
/**
* Run-once wrapper function for the workspace check to prompt to create an environment.
* @returns : True if we should prompt to c environment.
*/
export function isCreateEnvWorkspaceCheckNotRun(): boolean {
if (_alreadyCreateEnvCriteriaCheck) {
return false;
}
_alreadyCreateEnvCriteriaCheck = true;
return true;
}
Loading
Loading