Skip to content

Commit 31d186d

Browse files
committed
fix: support headless environment removal
1 parent cb454c9 commit 31d186d

10 files changed

Lines changed: 154 additions & 80 deletions

File tree

api/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Added `getPackageManager` to retrieve the registered package manager for an environment.
1313
- Added `PackageManagementInteractionOptions` with an optional `runHeadless?: boolean` property, mixed into `PackageManagementOptions`. When `true`, package management operations run without any user prompts or interaction — steps that would normally require input, such as selecting packages to install when none are specified, are skipped instead of prompting — for automated or headless scenarios such as integration tests.
14+
- Added `RemoveEnvironmentOptions` with an optional `runHeadless?: boolean` property to remove environments without a confirmation prompt in automated or headless scenarios.

examples/sample1/src/api.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,17 @@ export interface QuickCreateConfig {
329329
readonly detail?: string;
330330
}
331331

332+
/**
333+
* Options controlling environment removal.
334+
*/
335+
export interface RemoveEnvironmentOptions {
336+
/**
337+
* When `true`, removes the environment without prompting for confirmation.
338+
* Intended for automated or headless scenarios. Defaults to `false`.
339+
*/
340+
runHeadless?: boolean;
341+
}
342+
332343
/**
333344
* Interface representing an environment manager.
334345
*/
@@ -392,7 +403,7 @@ export interface EnvironmentManager {
392403
* @param environment - The Python environment to remove.
393404
* @returns A promise that resolves when the environment is removed.
394405
*/
395-
remove?(environment: PythonEnvironment): Promise<void>;
406+
remove?(environment: PythonEnvironment, options?: RemoveEnvironmentOptions): Promise<void>;
396407

397408
/**
398409
* Refreshes the list of Python environments within the specified scope.
@@ -881,9 +892,10 @@ export interface PythonEnvironmentManagementApi {
881892
* Remove a Python environment.
882893
*
883894
* @param environment The Python environment to remove.
895+
* @param options Optional parameters controlling environment removal.
884896
* @returns A promise that resolves when the environment has been removed.
885897
*/
886-
removeEnvironment(environment: PythonEnvironment): Promise<void>;
898+
removeEnvironment(environment: PythonEnvironment, options?: RemoveEnvironmentOptions): Promise<void>;
887899
}
888900

889901
export interface PythonEnvironmentsApi {

src/api.ts

Lines changed: 55 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,17 @@ export interface QuickCreateConfig {
345345
readonly detail?: string;
346346
}
347347

348+
/**
349+
* Options controlling environment removal.
350+
*/
351+
export interface RemoveEnvironmentOptions {
352+
/**
353+
* When `true`, removes the environment without prompting for confirmation.
354+
* Intended for automated or headless scenarios. Defaults to `false`.
355+
*/
356+
runHeadless?: boolean;
357+
}
358+
348359
/**
349360
* Interface representing an environment manager.
350361
*
@@ -425,7 +436,7 @@ export interface EnvironmentManager {
425436
* Invoked to delete the given environment. Typical triggers include an explicit user
426437
* action (such as a "Delete Environment" command) and programmatic removal via the API.
427438
*/
428-
remove?(environment: PythonEnvironment): Promise<void>;
439+
remove?(environment: PythonEnvironment, options?: RemoveEnvironmentOptions): Promise<void>;
429440

430441
/**
431442
* Refreshes the list of Python environments within the specified scope.
@@ -888,46 +899,47 @@ export interface PackageManagementInteractionOptions {
888899

889900
export type PackageManagementOptions = PackageManagementInteractionOptions &
890901
(
891-
| {
892-
/**
893-
* Upgrade the packages if they are already installed.
894-
*/
895-
upgrade?: boolean;
896-
897-
/**
898-
* Show option to skip package installation or uninstallation.
899-
*/
900-
showSkipOption?: boolean;
901-
/**
902-
* The list of packages to install.
903-
*/
904-
install: string[];
905-
906-
/**
907-
* The list of packages to uninstall.
908-
*/
909-
uninstall?: string[];
910-
}
911-
| {
912-
/**
913-
* Upgrade the packages if they are already installed.
914-
*/
915-
upgrade?: boolean;
916-
917-
/**
918-
* Show option to skip package installation or uninstallation.
919-
*/
920-
showSkipOption?: boolean;
921-
/**
922-
* The list of packages to install.
923-
*/
924-
install?: string[];
925-
926-
/**
927-
* The list of packages to uninstall.
928-
*/
929-
uninstall: string[];
930-
});
902+
| {
903+
/**
904+
* Upgrade the packages if they are already installed.
905+
*/
906+
upgrade?: boolean;
907+
908+
/**
909+
* Show option to skip package installation or uninstallation.
910+
*/
911+
showSkipOption?: boolean;
912+
/**
913+
* The list of packages to install.
914+
*/
915+
install: string[];
916+
917+
/**
918+
* The list of packages to uninstall.
919+
*/
920+
uninstall?: string[];
921+
}
922+
| {
923+
/**
924+
* Upgrade the packages if they are already installed.
925+
*/
926+
upgrade?: boolean;
927+
928+
/**
929+
* Show option to skip package installation or uninstallation.
930+
*/
931+
showSkipOption?: boolean;
932+
/**
933+
* The list of packages to install.
934+
*/
935+
install?: string[];
936+
937+
/**
938+
* The list of packages to uninstall.
939+
*/
940+
uninstall: string[];
941+
}
942+
);
931943

932944
/**
933945
* Options for creating a Python environment.
@@ -1026,9 +1038,10 @@ export interface PythonEnvironmentManagementApi {
10261038
* Remove a Python environment.
10271039
*
10281040
* @param environment The Python environment to remove.
1041+
* @param options Optional parameters controlling environment removal.
10291042
* @returns A promise that resolves when the environment has been removed.
10301043
*/
1031-
removeEnvironment(environment: PythonEnvironment): Promise<void>;
1044+
removeEnvironment(environment: PythonEnvironment, options?: RemoveEnvironmentOptions): Promise<void>;
10321045
}
10331046

10341047
export interface PythonEnvironmentsApi {

src/features/pythonApi.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
PythonTerminalCreateOptions,
2929
PythonTerminalExecutionOptions,
3030
RefreshEnvironmentsScope,
31+
RemoveEnvironmentOptions,
3132
ResolveEnvironmentContext,
3233
SetEnvironmentScope,
3334
} from '../api';
@@ -106,9 +107,7 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
106107

107108
this.previousProjects = current;
108109
if (added.length > 0 || removed.length > 0) {
109-
traceInfo(
110-
`Python API: Projects changed. Added: ${added.length}, Removed: ${removed.length}`,
111-
);
110+
traceInfo(`Python API: Projects changed. Added: ${added.length}, Removed: ${removed.length}`);
112111
this._onDidChangePythonProjects.fire({ added, removed });
113112
}
114113
}),
@@ -196,13 +195,13 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
196195
return result;
197196
}
198197
}
199-
async removeEnvironment(environment: PythonEnvironment): Promise<void> {
198+
async removeEnvironment(environment: PythonEnvironment, options?: RemoveEnvironmentOptions): Promise<void> {
200199
await waitForEnvManagerId([environment.envId.managerId]);
201200
const manager = this.envManagers.getEnvironmentManager(environment);
202201
if (!manager) {
203202
return Promise.reject(new Error('No environment manager found'));
204203
}
205-
return manager.remove(environment);
204+
return manager.remove(environment, options);
206205
}
207206
async refreshEnvironments(scope: RefreshEnvironmentsScope): Promise<void> {
208207
const currentScope = checkUri(scope) as RefreshEnvironmentsScope;

src/internal.api.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
PythonProjectCreator,
2727
QuickCreateConfig,
2828
RefreshEnvironmentsScope,
29+
RemoveEnvironmentOptions,
2930
ResolveEnvironmentContext,
3031
SetEnvironmentScope,
3132
} from './api';
@@ -208,9 +209,9 @@ export class InternalEnvironmentManager implements EnvironmentManager {
208209
return this.manager.remove !== undefined;
209210
}
210211

211-
remove(scope: PythonEnvironment): Promise<void> {
212+
remove(scope: PythonEnvironment, options?: RemoveEnvironmentOptions): Promise<void> {
212213
return this.manager.remove
213-
? this.manager.remove(scope)
214+
? this.manager.remove(scope, options)
214215
: Promise.reject(new RemoveEnvironmentNotSupported(`Remove Environment not supported by: ${this.id}`));
215216
}
216217

src/managers/builtin/venvManager.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
import * as fs from 'fs/promises';
22
import * as path from 'path';
3-
import {
4-
EventEmitter,
5-
l10n,
6-
LogOutputChannel,
7-
MarkdownString,
8-
ProgressLocation,
9-
ThemeIcon,
10-
Uri,
11-
} from 'vscode';
3+
import { EventEmitter, l10n, LogOutputChannel, MarkdownString, ProgressLocation, ThemeIcon, Uri } from 'vscode';
124
import {
135
CreateEnvironmentOptions,
146
CreateEnvironmentScope,
@@ -24,6 +16,7 @@ import {
2416
PythonProject,
2517
QuickCreateConfig,
2618
RefreshEnvironmentsScope,
19+
RemoveEnvironmentOptions,
2720
ResolveEnvironmentContext,
2821
SetEnvironmentScope,
2922
} from '../../api';
@@ -265,11 +258,11 @@ export class VenvManager implements EnvironmentManager {
265258
/**
266259
* Removes the specified Python environment, updates internal collections, and fires change events as needed.
267260
*/
268-
async remove(environment: PythonEnvironment): Promise<void> {
261+
async remove(environment: PythonEnvironment, options?: RemoveEnvironmentOptions): Promise<void> {
269262
try {
270263
this.skipWatcherRefresh = true;
271264

272-
const isRemoved = await removeVenv(environment, this.log);
265+
const isRemoved = await removeVenv(environment, this.log, options);
273266
if (!isRemoved) {
274267
return;
275268
}

src/managers/builtin/venvUtils.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ import {
1111
ThemeIcon,
1212
Uri,
1313
} from 'vscode';
14-
import { EnvironmentManager, PythonEnvironment, PythonEnvironmentApi, PythonEnvironmentInfo } from '../../api';
14+
import {
15+
EnvironmentManager,
16+
PythonEnvironment,
17+
PythonEnvironmentApi,
18+
PythonEnvironmentInfo,
19+
RemoveEnvironmentOptions,
20+
} from '../../api';
1521
import { ENVS_EXTENSION_ID } from '../../common/constants';
1622
import { Common, VenvManagerStrings } from '../../common/localize';
1723
import { traceInfo, traceVerbose } from '../../common/logging';
@@ -553,7 +559,11 @@ async function validateVenvRemovalPath(envPath: string, log: LogOutputChannel):
553559
return undefined;
554560
}
555561

556-
export async function removeVenv(environment: PythonEnvironment, log: LogOutputChannel): Promise<boolean> {
562+
export async function removeVenv(
563+
environment: PythonEnvironment,
564+
log: LogOutputChannel,
565+
options?: RemoveEnvironmentOptions,
566+
): Promise<boolean> {
557567
const pythonPath = os.platform() === 'win32' ? 'python.exe' : 'python';
558568

559569
const envFsPath = path.normalize(environment.environmentPath.fsPath);
@@ -568,15 +578,19 @@ export async function removeVenv(environment: PythonEnvironment, log: LogOutputC
568578
// Normalize path for UI display - ensure forward slashes on Windows
569579
const displayPath = normalizePath(envPath);
570580

571-
const confirm = await showWarningMessage(
572-
l10n.t('Are you sure you want to remove {0}?', displayPath),
573-
{
574-
modal: true,
575-
},
576-
{ title: Common.yes },
577-
{ title: Common.no, isCloseAffordance: true },
578-
);
579-
if (confirm?.title === Common.yes) {
581+
const confirmed =
582+
options?.runHeadless === true ||
583+
(
584+
await showWarningMessage(
585+
l10n.t('Are you sure you want to remove {0}?', displayPath),
586+
{
587+
modal: true,
588+
},
589+
{ title: Common.yes },
590+
{ title: Common.no, isCloseAffordance: true },
591+
)
592+
)?.title === Common.yes;
593+
if (confirmed) {
580594
const result = await withProgress(
581595
{
582596
location: ProgressLocation.Notification,

src/test/integration/packageManager.integration.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ for (const profile of profiles) {
8888
suiteTeardown(async () => {
8989
try {
9090
if (environment) {
91-
await api.removeEnvironment(environment);
91+
await api.removeEnvironment(environment, { runHeadless: true });
9292
}
9393
} finally {
9494
if (defaultEnvManagerUpdated) {

src/test/managers/builtin/venvManager.createRemove.unit.test.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,11 @@ function createManager(
4747
const baseManager = {
4848
getEnvironments: sinon.stub().resolves(baseEnvironments),
4949
} as any as EnvironmentManager;
50-
const manager = new VenvManager(
51-
{} as NativePythonFinder,
52-
api,
53-
baseManager,
54-
{ info: sinon.stub(), error: sinon.stub(), warn: sinon.stub() } as any,
55-
);
50+
const manager = new VenvManager({} as NativePythonFinder, api, baseManager, {
51+
info: sinon.stub(),
52+
error: sinon.stub(),
53+
warn: sinon.stub(),
54+
} as any);
5655
(manager as any)._initialized = { completed: true, promise: Promise.resolve() };
5756
(manager as any).collection = [];
5857
return manager;
@@ -221,6 +220,17 @@ suite('VenvManager.remove - orchestration', () => {
221220
assert.strictEqual(events[0][0].environment, env);
222221
});
223222

223+
test('forwards headless removal options to the removal helper', async () => {
224+
const manager = createManager();
225+
const env = environment();
226+
removeVenvStub.resolves(true);
227+
228+
await manager.remove(env, { runHeadless: true });
229+
230+
assert.strictEqual(removeVenvStub.firstCall.args[0], env);
231+
assert.deepStrictEqual(removeVenvStub.firstCall.args[2], { runHeadless: true });
232+
});
233+
224234
test('does not mutate state when the removal helper returns false', async () => {
225235
const manager = createManager();
226236
const env = environment();

0 commit comments

Comments
 (0)