Skip to content

Commit a741b91

Browse files
committed
feat: add conda quick create
1 parent 069159c commit a741b91

4 files changed

Lines changed: 170 additions & 26 deletions

File tree

src/common/localize.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ export namespace CondaStrings {
133133
export const condaCreateFailed = l10n.t('Failed to create conda environment');
134134
export const condaRemoveFailed = l10n.t('Failed to remove conda environment');
135135
export const condaExists = l10n.t('Environment already exists');
136+
137+
export const quickCreateCondaNoEnvRoot = l10n.t('No conda environment root found');
138+
export const quickCreateCondaNoName = l10n.t('Could not generate a name for env');
136139
}
137140

138141
export namespace ProjectCreatorString {

src/managers/builtin/venvUtils.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -466,11 +466,17 @@ export async function quickCreateVenv(
466466
manager: EnvironmentManager,
467467
baseEnv: PythonEnvironment,
468468
venvRoot: Uri,
469+
additionalPackages?: string[],
469470
): Promise<PythonEnvironment | undefined> {
470471
const project = api.getPythonProject(venvRoot);
471472

472473
sendTelemetryEvent(EventNames.VENV_CREATION, undefined, { creationType: 'quick' });
473474
const installables = await getProjectInstallable(api, project ? [project] : undefined);
475+
const allPackages = [];
476+
allPackages.push(...(installables?.flatMap((i) => i.args ?? []) ?? []));
477+
if (additionalPackages) {
478+
allPackages.push(...additionalPackages);
479+
}
474480
return await createWithProgress(
475481
nativeFinder,
476482
api,
@@ -479,7 +485,7 @@ export async function quickCreateVenv(
479485
baseEnv,
480486
venvRoot,
481487
path.join(venvRoot.fsPath, '.venv'),
482-
installables?.flatMap((i) => i.args ?? []),
488+
allPackages.length > 0 ? allPackages : undefined,
483489
);
484490
}
485491

@@ -490,7 +496,7 @@ export async function createPythonVenv(
490496
manager: EnvironmentManager,
491497
basePythons: PythonEnvironment[],
492498
venvRoot: Uri,
493-
options: { showQuickAndCustomOptions: boolean },
499+
options: { showQuickAndCustomOptions: boolean; additionalPackages?: string[] },
494500
): Promise<PythonEnvironment | undefined> {
495501
const sortedEnvs = ensureGlobalEnv(basePythons, log);
496502
const project = api.getPythonProject(venvRoot);
@@ -505,6 +511,11 @@ export async function createPythonVenv(
505511
} else if (customize === false) {
506512
sendTelemetryEvent(EventNames.VENV_CREATION, undefined, { creationType: 'quick' });
507513
const installables = await getProjectInstallable(api, project ? [project] : undefined);
514+
const allPackages = [];
515+
allPackages.push(...(installables?.flatMap((i) => i.args ?? []) ?? []));
516+
if (options.additionalPackages) {
517+
allPackages.push(...options.additionalPackages);
518+
}
508519
return await createWithProgress(
509520
nativeFinder,
510521
api,
@@ -513,7 +524,7 @@ export async function createPythonVenv(
513524
sortedEnvs[0],
514525
venvRoot,
515526
path.join(venvRoot.fsPath, '.venv'),
516-
installables?.flatMap((i) => i.args ?? []),
527+
allPackages.length > 0 ? allPackages : undefined,
517528
);
518529
} else {
519530
sendTelemetryEvent(EventNames.VENV_CREATION, undefined, { creationType: 'custom' });
@@ -550,8 +561,19 @@ export async function createPythonVenv(
550561
{ showSkipOption: true },
551562
project ? [project] : undefined,
552563
);
564+
const allPackages = [];
565+
allPackages.push(...(packages ?? []), ...(options.additionalPackages ?? []));
553566

554-
return await createWithProgress(nativeFinder, api, log, manager, basePython, venvRoot, envPath, packages);
567+
return await createWithProgress(
568+
nativeFinder,
569+
api,
570+
log,
571+
manager,
572+
basePython,
573+
venvRoot,
574+
envPath,
575+
allPackages.length > 0 ? allPackages : undefined,
576+
);
555577
}
556578

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

src/managers/conda/condaEnvManager.ts

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as path from 'path';
2-
import { Disposable, EventEmitter, LogOutputChannel, MarkdownString, ProgressLocation, Uri } from 'vscode';
2+
import { Disposable, EventEmitter, l10n, LogOutputChannel, MarkdownString, ProgressLocation, Uri } from 'vscode';
33
import {
4+
CreateEnvironmentOptions,
45
CreateEnvironmentScope,
56
DidChangeEnvironmentEventArgs,
67
DidChangeEnvironmentsEventArgs,
@@ -12,6 +13,7 @@ import {
1213
PythonEnvironment,
1314
PythonEnvironmentApi,
1415
PythonProject,
16+
QuickCreateConfig,
1517
RefreshEnvironmentsScope,
1618
ResolveEnvironmentContext,
1719
SetEnvironmentScope,
@@ -20,8 +22,11 @@ import {
2022
clearCondaCache,
2123
createCondaEnvironment,
2224
deleteCondaEnvironment,
25+
generateName,
2326
getCondaForGlobal,
2427
getCondaForWorkspace,
28+
getDefaultCondaPrefix,
29+
quickCreateConda,
2530
refreshCondaEnvs,
2631
resolveCondaPath,
2732
setCondaForGlobal,
@@ -32,6 +37,7 @@ import { NativePythonFinder } from '../common/nativePythonFinder';
3237
import { createDeferred, Deferred } from '../../common/utils/deferred';
3338
import { withProgress } from '../../common/window.apis';
3439
import { CondaStrings } from '../../common/localize';
40+
import { showErrorMessage } from '../../common/errors/utils';
3541

3642
export class CondaEnvManager implements EnvironmentManager, Disposable {
3743
private collection: PythonEnvironment[] = [];
@@ -116,29 +122,67 @@ export class CondaEnvManager implements EnvironmentManager, Disposable {
116122
return [];
117123
}
118124

119-
async create(context: CreateEnvironmentScope): Promise<PythonEnvironment | undefined> {
125+
quickCreateConfig(): QuickCreateConfig | undefined {
126+
if (!this.globalEnv) {
127+
return undefined;
128+
}
129+
130+
return {
131+
description: l10n.t('Create a conda virtual environment in workspace root'),
132+
detail: l10n.t('Uses Python version {0} and installs workspace dependencies.', this.globalEnv.version),
133+
};
134+
}
135+
136+
async create(
137+
context: CreateEnvironmentScope,
138+
options?: CreateEnvironmentOptions,
139+
): Promise<PythonEnvironment | undefined> {
120140
try {
121-
const result = await createCondaEnvironment(
122-
this.api,
123-
this.log,
124-
this,
125-
context === 'global' ? undefined : context,
126-
);
127-
if (!result) {
128-
return undefined;
141+
let result: PythonEnvironment | undefined;
142+
if (options?.quickCreate) {
143+
let envRoot: string | undefined = undefined;
144+
let name: string | undefined = './.conda';
145+
if (context === 'global' || (Array.isArray(context) && context.length > 1)) {
146+
envRoot = await getDefaultCondaPrefix();
147+
name = await generateName(envRoot);
148+
} else {
149+
const folder = this.api.getPythonProject(context instanceof Uri ? context : context[0]);
150+
envRoot = folder?.uri.fsPath;
151+
}
152+
if (!envRoot) {
153+
showErrorMessage(CondaStrings.quickCreateCondaNoEnvRoot);
154+
return undefined;
155+
}
156+
if (!name) {
157+
showErrorMessage(CondaStrings.quickCreateCondaNoName);
158+
return undefined;
159+
}
160+
result = await quickCreateConda(this.api, this.log, this, envRoot, name, options?.additionalPackages);
161+
} else {
162+
result = await createCondaEnvironment(
163+
this.api,
164+
this.log,
165+
this,
166+
context === 'global' ? undefined : context,
167+
);
129168
}
130-
this.disposablesMap.set(
131-
result.envId.id,
132-
new Disposable(() => {
133-
this.collection = this.collection.filter((env) => env.envId.id !== result.envId.id);
134-
Array.from(this.fsPathToEnv.entries())
135-
.filter(([, env]) => env.envId.id === result.envId.id)
136-
.forEach(([uri]) => this.fsPathToEnv.delete(uri));
137-
this.disposablesMap.delete(result.envId.id);
138-
}),
139-
);
140-
this.collection.push(result);
141-
this._onDidChangeEnvironments.fire([{ kind: EnvironmentChangeKind.add, environment: result }]);
169+
if (result) {
170+
this.disposablesMap.set(
171+
result.envId.id,
172+
new Disposable(() => {
173+
if (result) {
174+
this.collection = this.collection.filter((env) => env.envId.id !== result?.envId.id);
175+
Array.from(this.fsPathToEnv.entries())
176+
.filter(([, env]) => env.envId.id === result?.envId.id)
177+
.forEach(([uri]) => this.fsPathToEnv.delete(uri));
178+
this.disposablesMap.delete(result.envId.id);
179+
}
180+
}),
181+
);
182+
this.collection.push(result);
183+
this._onDidChangeEnvironments.fire([{ kind: EnvironmentChangeKind.add, environment: result }]);
184+
}
185+
142186
return result;
143187
} catch (error) {
144188
this.log.error('Failed to create conda environment:', error);

src/managers/conda/condaUtils.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,11 @@ async function getPrefixes(): Promise<string[]> {
223223
return prefixes;
224224
}
225225

226+
export async function getDefaultCondaPrefix(): Promise<string> {
227+
const prefixes = await getPrefixes();
228+
return prefixes.length > 0 ? prefixes[0] : path.join(os.homedir(), '.conda', 'envs');
229+
}
230+
226231
async function getVersion(root: string): Promise<string> {
227232
const files = await fse.readdir(path.join(root, 'conda-meta'));
228233
for (let file of files) {
@@ -645,6 +650,76 @@ async function createPrefixCondaEnvironment(
645650
);
646651
}
647652

653+
export async function generateName(fsPath: string): Promise<string | undefined> {
654+
let attempts = 0;
655+
while (attempts < 5) {
656+
const randomStr = Math.random().toString(36).substring(2);
657+
const name = `env_${randomStr}`;
658+
const prefix = path.join(fsPath, name);
659+
if (!(await fse.exists(prefix))) {
660+
return name;
661+
}
662+
}
663+
return undefined;
664+
}
665+
666+
export async function quickCreateConda(
667+
api: PythonEnvironmentApi,
668+
log: LogOutputChannel,
669+
manager: EnvironmentManager,
670+
fsPath: string,
671+
name: string,
672+
additionalPackages?: string[],
673+
): Promise<PythonEnvironment | undefined> {
674+
const prefix = path.join(fsPath, name);
675+
676+
return await withProgress(
677+
{
678+
location: ProgressLocation.Notification,
679+
title: `Creating conda environment: ${name}`,
680+
},
681+
async () => {
682+
try {
683+
const bin = os.platform() === 'win32' ? 'python.exe' : 'python';
684+
log.info(await runConda(['create', '--yes', '--prefix', prefix, 'python']));
685+
if (additionalPackages && additionalPackages.length > 0) {
686+
log.info(await runConda(['install', '--yes', '--prefix', prefix, ...additionalPackages]));
687+
}
688+
const version = await getVersion(prefix);
689+
690+
const environment = api.createPythonEnvironmentItem(
691+
{
692+
name: path.basename(prefix),
693+
environmentPath: Uri.file(prefix),
694+
displayName: `${version} (${name})`,
695+
displayPath: prefix,
696+
description: prefix,
697+
version,
698+
execInfo: {
699+
run: { executable: path.join(prefix, bin) },
700+
activatedRun: {
701+
executable: 'conda',
702+
args: ['run', '--live-stream', '-p', prefix, 'python'],
703+
},
704+
activation: [{ executable: 'conda', args: ['activate', prefix] }],
705+
deactivation: [{ executable: 'conda', args: ['deactivate'] }],
706+
},
707+
sysPrefix: prefix,
708+
group: 'Prefix',
709+
},
710+
manager,
711+
);
712+
return environment;
713+
} catch (e) {
714+
log.error('Failed to create conda environment', e);
715+
setImmediate(async () => {
716+
await showErrorMessage(CondaStrings.condaCreateFailed, log);
717+
});
718+
}
719+
},
720+
);
721+
}
722+
648723
export async function deleteCondaEnvironment(environment: PythonEnvironment, log: LogOutputChannel): Promise<boolean> {
649724
let args = ['env', 'remove', '--yes', '--prefix', environment.environmentPath.fsPath];
650725
return await withProgress(

0 commit comments

Comments
 (0)