Skip to content

Commit 794c723

Browse files
committed
feat: add conda quick create
1 parent b0d6dbd commit 794c723

File tree

4 files changed

+170
-26
lines changed

4 files changed

+170
-26
lines changed

src/common/localize.ts

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

140143
export namespace ProjectCreatorString {

src/managers/builtin/venvUtils.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -470,11 +470,17 @@ export async function quickCreateVenv(
470470
manager: EnvironmentManager,
471471
baseEnv: PythonEnvironment,
472472
venvRoot: Uri,
473+
additionalPackages?: string[],
473474
): Promise<PythonEnvironment | undefined> {
474475
const project = api.getPythonProject(venvRoot);
475476

476477
sendTelemetryEvent(EventNames.VENV_CREATION, undefined, { creationType: 'quick' });
477478
const installables = await getProjectInstallable(api, project ? [project] : undefined);
479+
const allPackages = [];
480+
allPackages.push(...(installables?.flatMap((i) => i.args ?? []) ?? []));
481+
if (additionalPackages) {
482+
allPackages.push(...additionalPackages);
483+
}
478484
return await createWithProgress(
479485
nativeFinder,
480486
api,
@@ -483,7 +489,7 @@ export async function quickCreateVenv(
483489
baseEnv,
484490
venvRoot,
485491
path.join(venvRoot.fsPath, '.venv'),
486-
installables?.flatMap((i) => i.args ?? []),
492+
allPackages.length > 0 ? allPackages : undefined,
487493
);
488494
}
489495

@@ -494,7 +500,7 @@ export async function createPythonVenv(
494500
manager: EnvironmentManager,
495501
basePythons: PythonEnvironment[],
496502
venvRoot: Uri,
497-
options: { showQuickAndCustomOptions: boolean },
503+
options: { showQuickAndCustomOptions: boolean; additionalPackages?: string[] },
498504
): Promise<PythonEnvironment | undefined> {
499505
const sortedEnvs = ensureGlobalEnv(basePythons, log);
500506
const project = api.getPythonProject(venvRoot);
@@ -509,6 +515,11 @@ export async function createPythonVenv(
509515
} else if (customize === false) {
510516
sendTelemetryEvent(EventNames.VENV_CREATION, undefined, { creationType: 'quick' });
511517
const installables = await getProjectInstallable(api, project ? [project] : undefined);
518+
const allPackages = [];
519+
allPackages.push(...(installables?.flatMap((i) => i.args ?? []) ?? []));
520+
if (options.additionalPackages) {
521+
allPackages.push(...options.additionalPackages);
522+
}
512523
return await createWithProgress(
513524
nativeFinder,
514525
api,
@@ -517,7 +528,7 @@ export async function createPythonVenv(
517528
sortedEnvs[0],
518529
venvRoot,
519530
path.join(venvRoot.fsPath, '.venv'),
520-
{ install: installables?.flatMap((i) => i.args ?? []), uninstall: [] },
531+
{ install: allPackages, uninstall: [] },
521532
);
522533
} else {
523534
sendTelemetryEvent(EventNames.VENV_CREATION, undefined, { creationType: 'custom' });
@@ -554,8 +565,19 @@ export async function createPythonVenv(
554565
{ showSkipOption: true, install: [] },
555566
project ? [project] : undefined,
556567
);
568+
const allPackages = [];
569+
allPackages.push(...(packages ?? []), ...(options.additionalPackages ?? []));
557570

558-
return await createWithProgress(nativeFinder, api, log, manager, basePython, venvRoot, envPath, packages);
571+
return await createWithProgress(
572+
nativeFinder,
573+
api,
574+
log,
575+
manager,
576+
basePython,
577+
venvRoot,
578+
envPath,
579+
allPackages.length > 0 ? allPackages : undefined,
580+
);
559581
}
560582

561583
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
@@ -222,6 +222,11 @@ async function getPrefixes(): Promise<string[]> {
222222
return prefixes;
223223
}
224224

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

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

0 commit comments

Comments
 (0)