Skip to content

Commit 4495ec7

Browse files
committed
API: create environment with name
1 parent e4ab62b commit 4495ec7

15 files changed

Lines changed: 378 additions & 110 deletions

‎api/CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to the `@vscode/python-environments` API package are documen
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.5.0]
9+
10+
### Added
11+
12+
- Added optional `CreateEnvironmentOptions.name` so API consumers can request a specific name when creating an environment.
13+
814
## [1.4.0]
915

1016
### Changed

‎api/package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎api/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@vscode/python-environments",
33
"description": "An API facade for the Python Environments extension in VS Code",
4-
"version": "1.4.0",
4+
"version": "1.5.0",
55
"author": {
66
"name": "Microsoft Corporation"
77
},

‎docs/README.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,11 +274,13 @@ applies to.
274274

275275
| Field | Type | Required | Description |
276276
| --- | --- | --- | --- |
277+
| `name` | `string` | No | Non-empty path segment to use as the new environment's name. Directory separators, `.` and `..` are rejected. When supplied, the manager uses that name without prompting for another. When omitted, the manager may prompt for a name or choose a default. |
277278
| `quickCreate` | `boolean` | No | `true` creates without any prompts. `false` means the user explicitly declined quick create, so prompts are allowed. `undefined` leaves the decision to the manager, which may offer quick create. |
278279
| `additionalPackages` | `string[]` | No | Packages to install in addition to whatever the manager installs by default. |
279280

280281
```typescript
281282
const env = await api.createEnvironment(projectUri, {
283+
name: 'analysis-env',
282284
quickCreate: true,
283285
additionalPackages: ['requests', 'pytest'],
284286
});
@@ -506,7 +508,7 @@ createEnvironment(
506508
| Parameter | Type | Required | Description |
507509
| --- | --- | --- | --- |
508510
| `scope` | [`CreateEnvironmentScope`](#scope-types) | Yes | `Uri` or `Uri[]` for the projects the environment is created for; `'global'` creates one outside any project. |
509-
| `options` | [`CreateEnvironmentOptions`](#createenvironmentoptions) | No | Controls prompting (`quickCreate`) and extra packages (`additionalPackages`). |
511+
| `options` | [`CreateEnvironmentOptions`](#createenvironmentoptions) | No | Controls the environment name (`name`), prompting (`quickCreate`), and extra packages (`additionalPackages`). |
510512

511513
**Returns** `Promise<PythonEnvironment | undefined>` - `undefined` when no
512514
environment was created, for example because the user cancelled the flow.
@@ -516,6 +518,7 @@ errors as well as `undefined`.
516518

517519
```typescript
518520
const created = await api.createEnvironment(projectUri, {
521+
name: 'analysis-env',
519522
quickCreate: true,
520523
additionalPackages: ['requests'],
521524
});

‎src/extensionApi.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,17 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
156156
scope: CreateEnvironmentScope,
157157
options: CreateEnvironmentOptions | undefined,
158158
): Promise<PythonEnvironment | undefined> {
159+
if (
160+
options?.name !== undefined &&
161+
(options.name.trim().length === 0 ||
162+
options.name === '.' ||
163+
options.name === '..' ||
164+
options.name.includes('/') ||
165+
options.name.includes('\\') ||
166+
options.name.includes('\0'))
167+
) {
168+
throw new Error('Environment name must be a non-empty path segment');
169+
}
159170
if (scope === 'global' || (!Array.isArray(scope) && scope instanceof Uri)) {
160171
await waitForEnvManager(scope === 'global' ? undefined : [scope]);
161172
const manager = this.envManagers.getEnvironmentManager(scope === 'global' ? undefined : scope);

‎src/managers/builtin/venvManager.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,13 +185,18 @@ export class VenvManager implements EnvironmentManager {
185185
this.globalEnv,
186186
venvRoot,
187187
options?.additionalPackages,
188+
options?.name,
188189
);
189190
}
190191
} else {
191192
// If quickCreate is not set that means the user triggered this method from
192193
// environment manager View, by selecting the venv manager.
193194
result = await createPythonVenv(this.nativeFinder, this.api, this.log, this, globals, venvRoot, {
194195
showQuickAndCustomOptions: options?.quickCreate === undefined,
196+
...(options?.name === undefined ? {} : { name: options.name }),
197+
...(options?.additionalPackages === undefined
198+
? {}
199+
: { additionalPackages: options.additionalPackages }),
195200
});
196201
}
197202

‎src/managers/builtin/venvStepBasedFlow.ts‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ interface VenvCreationState {
3131

3232
// Name for the venv
3333
venvName?: string;
34+
suppliedName?: boolean;
3435

3536
// Packages to install in the venv
3637
// undefined = not yet set, null = user canceled during package selection
@@ -161,8 +162,7 @@ async function selectBasePython(state: VenvCreationState): Promise<StepFunction
161162

162163
state.basePython = basePython;
163164

164-
// Next step: input venv name
165-
return enterEnvironmentName;
165+
return state.venvName === undefined ? enterEnvironmentName : selectPackages;
166166
} catch (ex) {
167167
if (ex === QuickInputButtons.Back) {
168168
// Go back to create type selection if we came from there
@@ -269,8 +269,7 @@ async function selectPackages(state: VenvCreationState): Promise<StepFunction |
269269
return null;
270270
} catch (ex) {
271271
if (ex === QuickInputButtons.Back) {
272-
// Go back to environment name input
273-
return enterEnvironmentName;
272+
return state.suppliedName ? selectBasePython : enterEnvironmentName;
274273
}
275274
throw ex;
276275
}
@@ -299,7 +298,7 @@ export async function createStepBasedVenvFlow(
299298
manager: EnvironmentManager,
300299
basePythons: PythonEnvironment[],
301300
venvRoot: Uri,
302-
options: { showQuickAndCustomOptions: boolean; additionalPackages?: string[] },
301+
options: { showQuickAndCustomOptions: boolean; additionalPackages?: string[]; name?: string },
303302
): Promise<CreateEnvironmentResult | undefined> {
304303
// Sort and filter available Python environments
305304
const sortedEnvs = ensureGlobalEnv(basePythons, log);
@@ -308,13 +307,18 @@ export async function createStepBasedVenvFlow(
308307
envCreationErr: 'No suitable Python environments found',
309308
};
310309
}
310+
if (options.name !== undefined && (await fse.pathExists(path.join(venvRoot.fsPath, options.name)))) {
311+
return { envCreationErr: VenvManagerStrings.venvNameErrorExists };
312+
}
311313

312314
// Initialize the state object that will track user selections
313315
const state: VenvCreationState = {
314316
sortedEnvs, // Store sorted environments in state to avoid re-sorting
315317
api, // Store API reference for package selection
316318
project: [api.getPythonProject(venvRoot)].filter(Boolean) as PythonProject[], // Get project for venvRoot
317319
venvRoot, // Store venvRoot for path validation
320+
venvName: options.name,
321+
suppliedName: options.name !== undefined,
318322
};
319323

320324
try {
@@ -335,8 +339,7 @@ export async function createStepBasedVenvFlow(
335339
if (state.isQuickCreate && state.basePython) {
336340
// Use quick create flow
337341
sendTelemetryEvent(EventNames.VENV_CREATION, undefined, { creationType: 'quick' });
338-
// Use the default .venv name for quick create
339-
const quickEnvPath = path.join(venvRoot.fsPath, '.venv');
342+
const quickEnvPath = path.join(venvRoot.fsPath, options.name ?? '.venv');
340343

341344
// Get workspace dependencies to install
342345
const project = api.getPythonProject(venvRoot);

‎src/managers/builtin/venvUtils.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,7 @@ export async function quickCreateVenv(
514514
baseEnv: PythonEnvironment,
515515
venvRoot: Uri,
516516
additionalPackages?: string[],
517+
name?: string,
517518
): Promise<CreateEnvironmentResult | undefined> {
518519
const project = api.getPythonProject(venvRoot);
519520

@@ -535,9 +536,12 @@ export async function quickCreateVenv(
535536
return undefined;
536537
}
537538

538-
// Check if .venv already exists
539-
let venvPath = path.join(venvRoot.fsPath, '.venv');
539+
const requestedName = name ?? '.venv';
540+
let venvPath = path.join(venvRoot.fsPath, requestedName);
540541
if (await fsapi.pathExists(venvPath)) {
542+
if (name !== undefined) {
543+
return { envCreationErr: VenvManagerStrings.venvNameErrorExists };
544+
}
541545
// increment to create a unique name, e.g. .venv-1
542546
let i = 1;
543547
while (await fsapi.pathExists(`${venvPath}-${i}`)) {
@@ -560,7 +564,7 @@ export async function createPythonVenv(
560564
manager: EnvironmentManager,
561565
basePythons: PythonEnvironment[],
562566
venvRoot: Uri,
563-
options: { showQuickAndCustomOptions: boolean; additionalPackages?: string[] },
567+
options: { showQuickAndCustomOptions: boolean; additionalPackages?: string[]; name?: string },
564568
): Promise<CreateEnvironmentResult | undefined> {
565569
return createStepBasedVenvFlow(nativeFinder, api, log, manager, basePythons, venvRoot, options);
566570
}

‎src/managers/conda/condaEnvManager.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,12 @@ export class CondaEnvManager implements EnvironmentManager, Disposable {
217217
let result: PythonEnvironment | undefined;
218218
if (options?.quickCreate) {
219219
let envRoot: string | undefined = undefined;
220-
let name: string | undefined = './.conda';
220+
let name: string | undefined = options.name ?? './.conda';
221221
if (context === 'global' || (Array.isArray(context) && context.length > 1)) {
222222
envRoot = await getDefaultCondaPrefix();
223-
name = await generateName(envRoot);
223+
if (options.name === undefined) {
224+
name = await generateName(envRoot);
225+
}
224226
} else {
225227
const folder = this.api.getPythonProject(context instanceof Uri ? context : context[0]);
226228
envRoot = folder?.uri.fsPath;
@@ -240,6 +242,7 @@ export class CondaEnvManager implements EnvironmentManager, Disposable {
240242
this.log,
241243
this,
242244
context === 'global' ? undefined : context,
245+
options?.name,
243246
);
244247
}
245248
if (result) {

‎src/managers/conda/condaStepBasedFlow.ts‎

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ interface CondaCreationState {
3535

3636
// For named environments
3737
envName?: string;
38+
suppliedName?: boolean;
39+
cancelled?: boolean;
3840

3941
// For prefix environments
4042
prefix?: string;
@@ -82,6 +84,7 @@ async function selectEnvironmentType(state: CondaCreationState): Promise<StepFun
8284
)) as QuickPickItem | undefined;
8385

8486
if (!selection) {
87+
state.cancelled = true;
8588
return null;
8689
}
8790

@@ -128,16 +131,23 @@ async function selectPythonVersion(state: CondaCreationState): Promise<StepFunct
128131
});
129132

130133
if (!selection) {
134+
state.cancelled = true;
131135
return null;
132136
}
133137

134138
state.pythonVersion = (selection as QuickPickItem).description;
135139

136140
// Next step depends on environment type
137-
return state.envType === getCondaNamedLabel() ? enterEnvironmentName : selectLocation;
141+
if (state.envType === getCondaNamedLabel()) {
142+
return state.envName === undefined ? enterEnvironmentName : null;
143+
}
144+
return selectLocation;
138145
} catch (ex) {
139146
if (ex === QuickInputButtons.Back) {
140-
// Go back to environment type selection
147+
if (state.suppliedName) {
148+
state.cancelled = true;
149+
return null;
150+
}
141151
return selectEnvironmentType;
142152
}
143153
throw ex;
@@ -280,28 +290,35 @@ export async function createStepBasedCondaFlow(
280290
log: LogOutputChannel,
281291
manager: EnvironmentManager,
282292
uris?: Uri | Uri[],
293+
name?: string,
283294
): Promise<PythonEnvironment | undefined> {
284295
// Initialize the state object that will track user selections
285296
const state: CondaCreationState = {
286297
api: api,
287298
uris: Array.isArray(uris) ? uris : uris ? [uris] : [],
299+
envType: name === undefined ? undefined : getCondaNamedLabel(),
300+
envName: name,
301+
suppliedName: name !== undefined,
288302
};
289303

290304
try {
291305
// Start with the first step
292-
let currentStep: StepFunction | null = selectEnvironmentType;
306+
let currentStep: StepFunction | null = name === undefined ? selectEnvironmentType : selectPythonVersion;
293307

294308
// Execute steps until completion or cancellation
295309
while (currentStep !== null) {
296310
currentStep = await currentStep(state);
297311
}
298312

313+
if (state.cancelled) {
314+
return undefined;
315+
}
316+
299317
// If we have all required data, create the environment
300318
if (state.envType === getCondaNamedLabel() && state.envName) {
301319
return await createNamedCondaEnvironment(api, log, manager, state.envName, state.pythonVersion);
302320
} else if (state.envType === CondaStrings.condaPrefix && state.prefix) {
303-
// For prefix environments, we need to pass the fsPath where the environment will be created
304-
return await createPrefixCondaEnvironment(api, log, manager, state.fsPath, state.pythonVersion);
321+
return await createPrefixCondaEnvironment(api, log, manager, state.prefix, state.pythonVersion);
305322
}
306323

307324
// If we get here, the flow was likely cancelled
@@ -310,7 +327,7 @@ export async function createStepBasedCondaFlow(
310327
if (ex === QuickInputButtons.Back) {
311328
// This should not happen as back navigation is handled within each step
312329
// But if it does, restart the flow
313-
return await createStepBasedCondaFlow(api, log, manager, uris);
330+
return await createStepBasedCondaFlow(api, log, manager, uris, name);
314331
}
315332
throw ex; // Re-throw other errors
316333
}

0 commit comments

Comments
 (0)