Skip to content

Commit 3fa5d4b

Browse files
authored
Support for Create Env command to re-create env for venv (#21829)
Closes #21827
1 parent 30e26c2 commit 3fa5d4b

File tree

11 files changed

+685
-40
lines changed

11 files changed

+685
-40
lines changed

src/client/common/utils/localize.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,13 @@ export namespace CreateEnv {
464464
export const error = l10n.t('Creating virtual environment failed with error.');
465465
export const tomlExtrasQuickPickTitle = l10n.t('Select optional dependencies to install from pyproject.toml');
466466
export const requirementsQuickPickTitle = l10n.t('Select dependencies to install');
467+
export const recreate = l10n.t('Recreate');
468+
export const recreateDescription = l10n.t('Delete existing ".venv" environment and create a new one');
469+
export const useExisting = l10n.t('Use Existing');
470+
export const useExistingDescription = l10n.t('Use existing ".venv" environment with no changes to it');
471+
export const existingVenvQuickPickPlaceholder = l10n.t('Use or Recreate existing environment?');
472+
export const deletingEnvironmentProgress = l10n.t('Deleting existing ".venv" environment...');
473+
export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".venv" environment.');
467474
}
468475

469476
export namespace Conda {

src/client/pythonEnvironments/creation/common/commonUtils.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
3+
import * as fs from 'fs-extra';
4+
import * as path from 'path';
5+
import { WorkspaceFolder } from 'vscode';
36
import { Commands } from '../../../common/constants';
47
import { Common } from '../../../common/utils/localize';
58
import { executeCommand } from '../../../common/vscodeApis/commandApis';
69
import { showErrorMessage } from '../../../common/vscodeApis/windowApis';
10+
import { isWindows } from '../../../common/platform/platformService';
711

812
export async function showErrorMessageWithLogs(message: string): Promise<void> {
913
const result = await showErrorMessage(message, Common.openOutputPanel, Common.selectPythonInterpreter);
@@ -13,3 +17,18 @@ export async function showErrorMessageWithLogs(message: string): Promise<void> {
1317
await executeCommand(Commands.Set_Interpreter);
1418
}
1519
}
20+
21+
export function getVenvPath(workspaceFolder: WorkspaceFolder): string {
22+
return path.join(workspaceFolder.uri.fsPath, '.venv');
23+
}
24+
25+
export async function hasVenv(workspaceFolder: WorkspaceFolder): Promise<boolean> {
26+
return fs.pathExists(getVenvPath(workspaceFolder));
27+
}
28+
29+
export function getVenvExecutable(workspaceFolder: WorkspaceFolder): string {
30+
if (isWindows()) {
31+
return path.join(getVenvPath(workspaceFolder), 'Scripts', 'python.exe');
32+
}
33+
return path.join(getVenvPath(workspaceFolder), 'bin', 'python');
34+
}

src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts

Lines changed: 105 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@ import { MultiStepAction, MultiStepNode, withProgress } from '../../../common/vs
1717
import { sendTelemetryEvent } from '../../../telemetry';
1818
import { EventName } from '../../../telemetry/constants';
1919
import { VenvProgressAndTelemetry, VENV_CREATED_MARKER, VENV_EXISTING_MARKER } from './venvProgressAndTelemetry';
20-
import { showErrorMessageWithLogs } from '../common/commonUtils';
21-
import { IPackageInstallSelection, pickPackagesToInstall } from './venvUtils';
20+
import { getVenvExecutable, showErrorMessageWithLogs } from '../common/commonUtils';
21+
import {
22+
ExistingVenvAction,
23+
IPackageInstallSelection,
24+
deleteEnvironment,
25+
pickExistingVenvAction,
26+
pickPackagesToInstall,
27+
} from './venvUtils';
2228
import { InputFlowAction } from '../../../common/utils/multiStepInput';
2329
import {
2430
CreateEnvironmentProvider,
@@ -150,33 +156,66 @@ export class VenvCreationProvider implements CreateEnvironmentProvider {
150156
undefined,
151157
);
152158

153-
let interpreter: string | undefined;
154-
const interpreterStep = new MultiStepNode(
159+
let existingVenvAction: ExistingVenvAction | undefined;
160+
const existingEnvStep = new MultiStepNode(
155161
workspaceStep,
156-
async () => {
157-
if (workspace) {
162+
async (context?: MultiStepAction) => {
163+
if (workspace && context === MultiStepAction.Continue) {
158164
try {
159-
interpreter = await this.interpreterQuickPick.getInterpreterViaQuickPick(
160-
workspace.uri,
161-
(i: PythonEnvironment) =>
162-
[
163-
EnvironmentType.System,
164-
EnvironmentType.MicrosoftStore,
165-
EnvironmentType.Global,
166-
EnvironmentType.Pyenv,
167-
].includes(i.envType) && i.type === undefined, // only global intepreters
168-
{
169-
skipRecommended: true,
170-
showBackButton: true,
171-
placeholder: CreateEnv.Venv.selectPythonPlaceHolder,
172-
title: null,
173-
},
174-
);
165+
existingVenvAction = await pickExistingVenvAction(workspace);
166+
return MultiStepAction.Continue;
175167
} catch (ex) {
176-
if (ex === InputFlowAction.back) {
168+
if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) {
169+
return ex;
170+
}
171+
throw ex;
172+
}
173+
} else if (context === MultiStepAction.Back) {
174+
return MultiStepAction.Back;
175+
}
176+
return MultiStepAction.Continue;
177+
},
178+
undefined,
179+
);
180+
workspaceStep.next = existingEnvStep;
181+
182+
let interpreter: string | undefined;
183+
const interpreterStep = new MultiStepNode(
184+
existingEnvStep,
185+
async (context?: MultiStepAction) => {
186+
if (workspace) {
187+
if (
188+
existingVenvAction === ExistingVenvAction.Recreate ||
189+
existingVenvAction === ExistingVenvAction.Create
190+
) {
191+
try {
192+
interpreter = await this.interpreterQuickPick.getInterpreterViaQuickPick(
193+
workspace.uri,
194+
(i: PythonEnvironment) =>
195+
[
196+
EnvironmentType.System,
197+
EnvironmentType.MicrosoftStore,
198+
EnvironmentType.Global,
199+
EnvironmentType.Pyenv,
200+
].includes(i.envType) && i.type === undefined, // only global intepreters
201+
{
202+
skipRecommended: true,
203+
showBackButton: true,
204+
placeholder: CreateEnv.Venv.selectPythonPlaceHolder,
205+
title: null,
206+
},
207+
);
208+
} catch (ex) {
209+
if (ex === InputFlowAction.back) {
210+
return MultiStepAction.Back;
211+
}
212+
interpreter = undefined;
213+
}
214+
} else if (existingVenvAction === ExistingVenvAction.UseExisting) {
215+
if (context === MultiStepAction.Back) {
177216
return MultiStepAction.Back;
178217
}
179-
interpreter = undefined;
218+
interpreter = getVenvExecutable(workspace);
180219
}
181220
}
182221

@@ -189,7 +228,7 @@ export class VenvCreationProvider implements CreateEnvironmentProvider {
189228
},
190229
undefined,
191230
);
192-
workspaceStep.next = interpreterStep;
231+
existingEnvStep.next = interpreterStep;
193232

194233
let addGitIgnore = true;
195234
let installPackages = true;
@@ -200,19 +239,23 @@ export class VenvCreationProvider implements CreateEnvironmentProvider {
200239
let installInfo: IPackageInstallSelection[] | undefined;
201240
const packagesStep = new MultiStepNode(
202241
interpreterStep,
203-
async () => {
242+
async (context?: MultiStepAction) => {
204243
if (workspace && installPackages) {
205-
try {
206-
installInfo = await pickPackagesToInstall(workspace);
207-
} catch (ex) {
208-
if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) {
209-
return ex;
244+
if (existingVenvAction !== ExistingVenvAction.UseExisting) {
245+
try {
246+
installInfo = await pickPackagesToInstall(workspace);
247+
} catch (ex) {
248+
if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) {
249+
return ex;
250+
}
251+
throw ex;
210252
}
211-
throw ex;
212-
}
213-
if (!installInfo) {
214-
traceVerbose('Virtual env creation exited during dependencies selection.');
215-
return MultiStepAction.Cancel;
253+
if (!installInfo) {
254+
traceVerbose('Virtual env creation exited during dependencies selection.');
255+
return MultiStepAction.Cancel;
256+
}
257+
} else if (context === MultiStepAction.Back) {
258+
return MultiStepAction.Back;
216259
}
217260
}
218261

@@ -227,6 +270,32 @@ export class VenvCreationProvider implements CreateEnvironmentProvider {
227270
throw action;
228271
}
229272

273+
if (workspace) {
274+
if (existingVenvAction === ExistingVenvAction.Recreate) {
275+
sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, {
276+
environmentType: 'venv',
277+
status: 'triggered',
278+
});
279+
if (await deleteEnvironment(workspace, interpreter)) {
280+
sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, {
281+
environmentType: 'venv',
282+
status: 'deleted',
283+
});
284+
} else {
285+
sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, {
286+
environmentType: 'venv',
287+
status: 'failed',
288+
});
289+
throw MultiStepAction.Cancel;
290+
}
291+
} else if (existingVenvAction === ExistingVenvAction.UseExisting) {
292+
sendTelemetryEvent(EventName.ENVIRONMENT_REUSE, undefined, {
293+
environmentType: 'venv',
294+
});
295+
return { path: getVenvExecutable(workspace), workspaceFolder: workspace };
296+
}
297+
}
298+
230299
const args = generateCommandArgs(installInfo, addGitIgnore);
231300

232301
return withProgress(
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as fs from 'fs-extra';
5+
import * as path from 'path';
6+
import { WorkspaceFolder } from 'vscode';
7+
import { traceError, traceInfo } from '../../../logging';
8+
import { getVenvPath, showErrorMessageWithLogs } from '../common/commonUtils';
9+
import { CreateEnv } from '../../../common/utils/localize';
10+
import { sleep } from '../../../common/utils/async';
11+
import { switchSelectedPython } from './venvSwitchPython';
12+
13+
async function tryDeleteFile(file: string): Promise<boolean> {
14+
try {
15+
if (!(await fs.pathExists(file))) {
16+
return true;
17+
}
18+
await fs.unlink(file);
19+
return true;
20+
} catch (err) {
21+
traceError(`Failed to delete file [${file}]:`, err);
22+
return false;
23+
}
24+
}
25+
26+
async function tryDeleteDir(dir: string): Promise<boolean> {
27+
try {
28+
if (!(await fs.pathExists(dir))) {
29+
return true;
30+
}
31+
await fs.rmdir(dir, {
32+
recursive: true,
33+
maxRetries: 10,
34+
retryDelay: 200,
35+
});
36+
return true;
37+
} catch (err) {
38+
traceError(`Failed to delete directory [${dir}]:`, err);
39+
return false;
40+
}
41+
}
42+
43+
export async function deleteEnvironmentNonWindows(workspaceFolder: WorkspaceFolder): Promise<boolean> {
44+
const venvPath = getVenvPath(workspaceFolder);
45+
if (await tryDeleteDir(venvPath)) {
46+
traceInfo(`Deleted venv dir: ${venvPath}`);
47+
return true;
48+
}
49+
showErrorMessageWithLogs(CreateEnv.Venv.errorDeletingEnvironment);
50+
return false;
51+
}
52+
53+
export async function deleteEnvironmentWindows(
54+
workspaceFolder: WorkspaceFolder,
55+
interpreter: string | undefined,
56+
): Promise<boolean> {
57+
const venvPath = getVenvPath(workspaceFolder);
58+
const venvPythonPath = path.join(venvPath, 'Scripts', 'python.exe');
59+
60+
if (await tryDeleteFile(venvPythonPath)) {
61+
traceInfo(`Deleted python executable: ${venvPythonPath}`);
62+
if (await tryDeleteDir(venvPath)) {
63+
traceInfo(`Deleted ".venv" dir: ${venvPath}`);
64+
return true;
65+
}
66+
67+
traceError(`Failed to delete ".venv" dir: ${venvPath}`);
68+
traceError(
69+
'This happens if the virtual environment is still in use, or some binary in the venv is still running.',
70+
);
71+
traceError(`Please delete the ".venv" manually: [${venvPath}]`);
72+
showErrorMessageWithLogs(CreateEnv.Venv.errorDeletingEnvironment);
73+
return false;
74+
}
75+
traceError(`Failed to delete python executable: ${venvPythonPath}`);
76+
traceError('This happens if the virtual environment is still in use.');
77+
78+
if (interpreter) {
79+
traceError('We will attempt to switch python temporarily to delete the ".venv"');
80+
81+
await switchSelectedPython(interpreter, workspaceFolder.uri, 'temporarily to delete the ".venv"');
82+
83+
traceInfo(`Attempting to delete ".venv" again: ${venvPath}`);
84+
const ms = 500;
85+
for (let i = 0; i < 5; i = i + 1) {
86+
traceInfo(`Waiting for ${ms}ms to let processes exit, before a delete attempt.`);
87+
await sleep(ms);
88+
if (await tryDeleteDir(venvPath)) {
89+
traceInfo(`Deleted ".venv" dir: ${venvPath}`);
90+
return true;
91+
}
92+
traceError(`Failed to delete ".venv" dir [${venvPath}] (attempt ${i + 1}/5).`);
93+
}
94+
} else {
95+
traceError(`Please delete the ".venv" dir manually: [${venvPath}]`);
96+
}
97+
showErrorMessageWithLogs(CreateEnv.Venv.errorDeletingEnvironment);
98+
return false;
99+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as path from 'path';
5+
import { Disposable, Uri } from 'vscode';
6+
import { createDeferred } from '../../../common/utils/async';
7+
import { getExtension } from '../../../common/vscodeApis/extensionsApi';
8+
import { PVSC_EXTENSION_ID, PythonExtension } from '../../../api/types';
9+
import { traceInfo } from '../../../logging';
10+
11+
export async function switchSelectedPython(interpreter: string, uri: Uri, purpose: string): Promise<void> {
12+
let dispose: Disposable | undefined;
13+
try {
14+
const deferred = createDeferred<void>();
15+
const api: PythonExtension = getExtension(PVSC_EXTENSION_ID)?.exports as PythonExtension;
16+
dispose = api.environments.onDidChangeActiveEnvironmentPath(async (e) => {
17+
if (path.normalize(e.path) === path.normalize(interpreter)) {
18+
traceInfo(`Switched to interpreter ${purpose}: ${interpreter}`);
19+
deferred.resolve();
20+
}
21+
});
22+
api.environments.updateActiveEnvironmentPath(interpreter, uri);
23+
traceInfo(`Switching interpreter ${purpose}: ${interpreter}`);
24+
await deferred.promise;
25+
} finally {
26+
dispose?.dispose();
27+
}
28+
}

0 commit comments

Comments
 (0)