Skip to content

Commit 8b61569

Browse files
authored
feat: add "Quick Create" option for venvs (#265)
closes #258 ![image](https://github.com/user-attachments/assets/ac6f339f-5234-49fc-bf0a-92e2f4ea10dc) /cc @cwebster-99 Need your input on the text
1 parent ab9e2df commit 8b61569

File tree

2 files changed

+113
-47
lines changed

2 files changed

+113
-47
lines changed

src/common/localize.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ export namespace VenvManagerStrings {
8989

9090
export const installEditable = l10n.t('Install project as editable');
9191
export const searchingDependencies = l10n.t('Searching for dependencies');
92+
93+
export const selectQuickOrCustomize = l10n.t('Select environment creation mode');
94+
export const quickCreate = l10n.t('Quick Create');
95+
export const quickCreateDescription = l10n.t('Create a virtual environment in the workspace root');
96+
export const customize = l10n.t('Custom');
97+
export const customizeDescription = l10n.t('Choose python version, location, packages, name, etc.');
9298
}
9399

94100
export namespace SysManagerStrings {

src/managers/builtin/venvUtils.ts

Lines changed: 107 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
import { showErrorMessage } from '../../common/errors/utils';
3232
import { Common, VenvManagerStrings } from '../../common/localize';
3333
import { isUvInstalled, runUV, runPython } from './helpers';
34-
import { getWorkspacePackagesToInstall } from './pipUtils';
34+
import { getProjectInstallable, getWorkspacePackagesToInstall } from './pipUtils';
3535

3636
export const VENV_WORKSPACE_KEY = `${ENVS_EXTENSION_ID}:venv:WORKSPACE_SELECTED`;
3737
export const VENV_GLOBAL_KEY = `${ENVS_EXTENSION_ID}:venv:GLOBAL_SELECTED`;
@@ -347,6 +347,90 @@ export async function getGlobalVenvLocation(): Promise<Uri | undefined> {
347347
return undefined;
348348
}
349349

350+
async function createWithCustomization(version: string): Promise<boolean | undefined> {
351+
const selection: QuickPickItem | undefined = await showQuickPick(
352+
[
353+
{
354+
label: VenvManagerStrings.quickCreate,
355+
description: VenvManagerStrings.quickCreateDescription,
356+
detail: l10n.t('Uses Python version {0} and installs workspace dependencies.', version),
357+
},
358+
{
359+
label: VenvManagerStrings.customize,
360+
description: VenvManagerStrings.customizeDescription,
361+
},
362+
],
363+
{
364+
placeHolder: VenvManagerStrings.selectQuickOrCustomize,
365+
ignoreFocusOut: true,
366+
},
367+
);
368+
369+
if (selection === undefined) {
370+
return undefined;
371+
} else if (selection.label === VenvManagerStrings.quickCreate) {
372+
return false;
373+
}
374+
return true;
375+
}
376+
377+
async function createWithProgress(
378+
nativeFinder: NativePythonFinder,
379+
api: PythonEnvironmentApi,
380+
log: LogOutputChannel,
381+
manager: EnvironmentManager,
382+
basePython: PythonEnvironment,
383+
venvRoot: Uri,
384+
envPath: string,
385+
packages?: string[],
386+
) {
387+
const pythonPath =
388+
os.platform() === 'win32' ? path.join(envPath, 'Scripts', 'python.exe') : path.join(envPath, 'bin', 'python');
389+
390+
return await withProgress(
391+
{
392+
location: ProgressLocation.Notification,
393+
title: VenvManagerStrings.venvCreating,
394+
},
395+
async () => {
396+
try {
397+
const useUv = await isUvInstalled(log);
398+
if (basePython.execInfo?.run.executable) {
399+
if (useUv) {
400+
await runUV(
401+
['venv', '--verbose', '--seed', '--python', basePython.execInfo?.run.executable, envPath],
402+
venvRoot.fsPath,
403+
log,
404+
);
405+
} else {
406+
await runPython(
407+
basePython.execInfo.run.executable,
408+
['-m', 'venv', envPath],
409+
venvRoot.fsPath,
410+
manager.log,
411+
);
412+
}
413+
if (!(await fsapi.pathExists(pythonPath))) {
414+
log.error('no python executable found in virtual environment');
415+
throw new Error('no python executable found in virtual environment');
416+
}
417+
}
418+
419+
const resolved = await nativeFinder.resolve(pythonPath);
420+
const env = api.createPythonEnvironmentItem(await getPythonInfo(resolved), manager);
421+
if (packages && packages?.length > 0) {
422+
await api.installPackages(env, packages, { upgrade: false });
423+
}
424+
return env;
425+
} catch (e) {
426+
log.error(`Failed to create virtual environment: ${e}`);
427+
showErrorMessage(VenvManagerStrings.venvCreateFailed);
428+
return;
429+
}
430+
},
431+
);
432+
}
433+
350434
export async function createPythonVenv(
351435
nativeFinder: NativePythonFinder,
352436
api: PythonEnvironmentApi,
@@ -371,7 +455,27 @@ export async function createPythonVenv(
371455
return;
372456
}
373457

374-
const basePython = await pickEnvironmentFrom(sortEnvironments(filtered));
458+
const sortedEnvs = sortEnvironments(filtered);
459+
const project = api.getPythonProject(venvRoot);
460+
461+
const customize = await createWithCustomization(sortedEnvs[0].version);
462+
if (customize === undefined) {
463+
return;
464+
} else if (customize === false) {
465+
const installables = await getProjectInstallable(api, project ? [project] : undefined);
466+
return await createWithProgress(
467+
nativeFinder,
468+
api,
469+
log,
470+
manager,
471+
sortedEnvs[0],
472+
venvRoot,
473+
path.join(venvRoot.fsPath, '.venv'),
474+
installables?.flatMap((i) => i.args ?? []),
475+
);
476+
}
477+
478+
const basePython = await pickEnvironmentFrom(sortedEnvs);
375479
if (!basePython || !basePython.execInfo) {
376480
log.error('No base python selected, cannot create virtual environment.');
377481
return;
@@ -396,58 +500,14 @@ export async function createPythonVenv(
396500
}
397501

398502
const envPath = path.join(venvRoot.fsPath, name);
399-
const pythonPath =
400-
os.platform() === 'win32' ? path.join(envPath, 'Scripts', 'python.exe') : path.join(envPath, 'bin', 'python');
401503

402-
const project = api.getPythonProject(venvRoot);
403504
const packages = await getWorkspacePackagesToInstall(
404505
api,
405506
{ showSkipOption: true },
406507
project ? [project] : undefined,
407508
);
408509

409-
return await withProgress(
410-
{
411-
location: ProgressLocation.Notification,
412-
title: VenvManagerStrings.venvCreating,
413-
},
414-
async () => {
415-
try {
416-
const useUv = await isUvInstalled(log);
417-
if (basePython.execInfo?.run.executable) {
418-
if (useUv) {
419-
await runUV(
420-
['venv', '--verbose', '--seed', '--python', basePython.execInfo?.run.executable, envPath],
421-
venvRoot.fsPath,
422-
log,
423-
);
424-
} else {
425-
await runPython(
426-
basePython.execInfo.run.executable,
427-
['-m', 'venv', envPath],
428-
venvRoot.fsPath,
429-
manager.log,
430-
);
431-
}
432-
if (!(await fsapi.pathExists(pythonPath))) {
433-
log.error('no python executable found in virtual environment');
434-
throw new Error('no python executable found in virtual environment');
435-
}
436-
}
437-
438-
const resolved = await nativeFinder.resolve(pythonPath);
439-
const env = api.createPythonEnvironmentItem(await getPythonInfo(resolved), manager);
440-
if (packages && packages?.length > 0) {
441-
await api.installPackages(env, packages, { upgrade: false });
442-
}
443-
return env;
444-
} catch (e) {
445-
log.error(`Failed to create virtual environment: ${e}`);
446-
showErrorMessage(VenvManagerStrings.venvCreateFailed);
447-
return;
448-
}
449-
},
450-
);
510+
return await createWithProgress(nativeFinder, api, log, manager, basePython, venvRoot, envPath, packages);
451511
}
452512

453513
export async function removeVenv(environment: PythonEnvironment, log: LogOutputChannel): Promise<boolean> {

0 commit comments

Comments
 (0)