Skip to content

Commit b17b529

Browse files
committed
Streamline conda commands
1 parent 3f1afe2 commit b17b529

7 files changed

Lines changed: 347 additions & 58 deletions

File tree

‎SETTINGS_ARCHITECTURE.md‎

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
# Class-Based Command Architecture with Three-Level Hierarchy
2+
3+
## Overview
4+
5+
Implemented package management commands using a three-level class hierarchy that separates concerns cleanly:
6+
7+
1. **Base class** (`PackageManagerCommand`) — minimal shared interface
8+
2. **Template classes** (`InstallCommand`, `ListCommand`, etc.) — load command-specific settings
9+
3. **Concrete classes** (`PipInstallCommand`, `CondaInstallCommand`, etc.) — implement package-manager-specific logic
10+
11+
This approach stores persisting arguments (like `indexUrl`) as instance properties while keeping ephemeral arguments (like packages) passed to `execute()`.
12+
13+
## Architecture Components
14+
15+
### 1. Base Class
16+
17+
**File**: `src/managers/base/commands/packageManagerCommand.ts`
18+
19+
```typescript
20+
interface CommandConstructorOptions {
21+
pythonExecutable: string;
22+
configSection?: string;
23+
log?: LogOutputChannel;
24+
}
25+
26+
abstract class PackageManagerCommand {
27+
protected pythonExecutable: string;
28+
protected log?: LogOutputChannel;
29+
protected timeout: number = 300000;
30+
protected config?: WorkspaceConfiguration;
31+
32+
constructor(options: CommandConstructorOptions) {
33+
this.pythonExecutable = options.pythonExecutable;
34+
this.log = options.log;
35+
// load command-specific settings (including executionTimeout) from configuration
36+
}
37+
38+
protected abstract buildCommand(ephemeralArgs: unknown): string[];
39+
}
40+
```
41+
42+
Minimal interface: only shared across all commands.
43+
44+
### 2. Template Classes
45+
46+
Each command type (install, uninstall, list, etc.) has a template class that:
47+
48+
- Loads its own command-specific settings from VS Code config
49+
- Defines the execute() interface (signature varies per command)
50+
- Is abstract (not instantiable directly)
51+
52+
#### InstallCommand Template
53+
54+
```typescript
55+
abstract class InstallCommand extends PackageManagerCommand {
56+
protected settings: CommandSettings;
57+
58+
constructor(options: CommandConstructorOptions) {
59+
super(options);
60+
const config = getConfiguration('python-envs.packageManager.installCommandArgs');
61+
this.settings = {
62+
executionTimeout: config.get<number>('executionTimeout', 300000),
63+
verboseOutput: config.get<boolean>('verboseOutput', false),
64+
retryOnFailure: config.get<boolean>('retryOnFailure', true),
65+
maxRetries: config.get<number>('maxRetries', 1),
66+
};
67+
}
68+
69+
abstract execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise<void>;
70+
}
71+
```
72+
73+
### 3. Concrete Classes
74+
75+
Each concrete class implements `buildCommand()` and `execute()` with package-manager-specific logic.
76+
77+
#### PipInstallCommand (Concrete)
78+
79+
```typescript
80+
export class PipInstallCommand extends InstallCommand {
81+
private indexUrl?: string; // Persisting argument
82+
83+
constructor(options: CommandConstructorOptions) {
84+
super(options);
85+
const config = getConfiguration('python-envs.packageManager');
86+
this.indexUrl = config.get<string>('indexUrl'); // Load global config
87+
}
88+
89+
// buildCommand uses persisting args (indexUrl) + ephemeral args (packages, upgrade)
90+
protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] {
91+
let args = ['-m', 'pip', 'install'];
92+
93+
if (this.indexUrl) {
94+
args.push('--index-url', this.indexUrl);
95+
}
96+
97+
if (ephemeralArgs.upgrade) {
98+
args.push('--upgrade');
99+
}
100+
101+
const processedArgs = processEditableInstallArgs(ephemeralArgs.packages.map((pkg) => pkg.packageName));
102+
args.push(...processedArgs);
103+
104+
return args;
105+
}
106+
107+
// execute() spawns subprocess directly with runPython
108+
async execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise<void> {
109+
const args = this.buildCommand({ packages, upgrade });
110+
111+
await runPython(
112+
this.pythonExecutable,
113+
args,
114+
undefined,
115+
this.log,
116+
this.cancellationToken,
117+
this.settings.executionTimeout,
118+
);
119+
}
120+
}
121+
```
122+
123+
#### CondaInstallCommand (Concrete, Different Package Manager)
124+
125+
```typescript
126+
export class CondaInstallCommand extends InstallCommand {
127+
protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] {
128+
let args = ['install', '-y'];
129+
130+
if (ephemeralArgs.upgrade) {
131+
args.push('--upgrade');
132+
}
133+
134+
args.push(...ephemeralArgs.packages.map((p) => p.packageName));
135+
136+
return args;
137+
}
138+
139+
async execute(packages: { packageName: string; version?: string }[], upgrade?: boolean): Promise<void> {
140+
const args = this.buildCommand({ packages, upgrade });
141+
142+
await runPython(
143+
this.pythonExecutable, // conda executable
144+
args,
145+
undefined,
146+
this.log,
147+
this.cancellationToken,
148+
this.settings.executionTimeout,
149+
);
150+
}
151+
}
152+
```
153+
154+
## Separation of Concerns
155+
156+
### Persisting Arguments (Constructor)
157+
158+
- Loaded once, reused across multiple executions
159+
- Stored as instance properties
160+
- Examples: `pythonExecutable`, `indexUrl`, `settings`, `log`
161+
162+
### Ephemeral Arguments (Execute)
163+
164+
- Change per invocation
165+
- Passed to `execute()` method
166+
- Examples: `packages`, `packageName`, `pythonVersion`, `upgrade`
167+
168+
```typescript
169+
// Constructor: load persisting config
170+
const install = new PipInstallCommand({
171+
pythonExecutable: '/usr/bin/python3',
172+
log: logger,
173+
});
174+
175+
// execute(): pass ephemeral args
176+
await install.execute([{ packageName: 'numpy' }], true);
177+
await install.execute([{ packageName: 'pandas' }], false); // Same indexUrl reused
178+
```
179+
180+
## Usage Flow
181+
182+
1. **Executor creates command instance** with persisting options:
183+
184+
```typescript
185+
const install = new PipInstallCommand({
186+
pythonExecutable,
187+
log: context.log,
188+
cancellationToken: context.cancellationToken,
189+
});
190+
```
191+
192+
2. **Constructor**:
193+
- Calls `super(options)` to set pythonExecutable, log, cancellationToken
194+
- Loads indexUrl from global config (persisting)
195+
- Loads command-specific settings (timeout, retry, verbose)
196+
197+
3. **Caller invokes execute()** with ephemeral args:
198+
199+
```typescript
200+
await install.execute(packages, upgrade);
201+
```
202+
203+
4. **execute()**:
204+
- Calls `buildCommand()` with ephemeral args
205+
- Calls `runPython()` directly (no intermediate executeCommand function)
206+
- Settings applied via `this.settings.executionTimeout`
207+
208+
## Command Files
209+
210+
| File | Template | Concrete(s) |
211+
| ---------------------- | -------------------------- | --------------------------------------------------------- |
212+
| `commandSettings.ts` | — | `PackageManagerCommand` base, `CommandSettings` interface |
213+
| `install.ts` | `InstallCommand` | `PipInstallCommand` |
214+
| `uninstall.ts` | `UninstallCommand` | `PipUninstallCommand` |
215+
| `list.ts` | `ListCommand` | `PipListCommand` |
216+
| `version.ts` | `VersionCommand` | `PipVersionCommand` |
217+
| `availableVersions.ts` | `AvailableVersionsCommand` | `PipAvailableVersionsCommand` |
218+
| `listDirectNames.ts` | `ListDirectNamesCommand` | `PipListDirectNamesCommand` |
219+
220+
## Future: Conda and Poetry
221+
222+
When extending to conda and poetry, simply add new concrete classes:
223+
224+
```typescript
225+
// In conda/commands/install.ts
226+
export class CondaInstallCommand extends InstallCommand {
227+
protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] {
228+
// conda-specific argument building
229+
}
230+
async execute(packages, upgrade) {
231+
// conda-specific execution
232+
}
233+
}
234+
235+
// In poetry/commands/install.ts
236+
export class PoetryInstallCommand extends InstallCommand {
237+
protected buildCommand(ephemeralArgs: InstallEphemeralArgs): string[] {
238+
// poetry-specific argument building
239+
}
240+
async execute(packages, upgrade) {
241+
// poetry-specific execution
242+
}
243+
}
244+
```
245+
246+
Same template interface, different implementations per package manager.
247+
248+
## Key Design Decisions
249+
250+
✅ **Three-level hierarchy**: Base → Template → Concrete
251+
✅ **Persisting vs ephemeral**: Constructor for config, execute() for data
252+
✅ **Settings auto-load**: Each template loads its own command-specific settings
253+
✅ **Direct runPython**: No executeCommand intermediate function
254+
✅ **Command-specific indexUrl**: Only loaded by install commands, others ignore
255+
✅ **No stored results**: Commands return data directly, don't cache on instance
256+
✅ **Extensible**: Easy to add conda, poetry, uv variants by extending templates
257+
258+
## Executor Integration
259+
260+
**File**: `src/managers/builtin/commands/builtinCommandExecutor.ts`
261+
262+
```typescript
263+
export class BuiltinCommandExecutor {
264+
async executeCommands(
265+
environment: PythonEnvironment,
266+
commands: BuiltinManageCommand[],
267+
context: BuiltinCommandExecutionContext,
268+
): Promise<void> {
269+
const pythonExecutable = environment.execInfo?.run?.executable ?? 'python';
270+
271+
for (const command of commands) {
272+
await this.executeCommand(pythonExecutable, command, context);
273+
}
274+
}
275+
276+
private async executeCommand(
277+
pythonExecutable: string,
278+
command: BuiltinManageCommand,
279+
context: BuiltinCommandExecutionContext,
280+
): Promise<void> {
281+
if (command.kind === 'install') {
282+
// Create concrete class with persisting options
283+
const install = new PipInstallCommand({
284+
pythonExecutable,
285+
log: context.log,
286+
cancellationToken: context.cancellationToken,
287+
});
288+
// Execute with ephemeral args
289+
await install.execute(command.payload.packages, command.payload.upgrade);
290+
return;
291+
}
292+
// Similar for uninstall, list, etc.
293+
}
294+
}
295+
```
296+
297+
## Architecture Components
298+
299+
### 1. Base Class
300+
301+
**File**: `src/managers/builtin/commands/commandSettings.ts`
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { CommandConstructorOptions } from '../../base/commands/index';
2+
3+
export interface CondaCommandConstructorOptions extends CommandConstructorOptions {
4+
condaEnvironmentPath: string;
5+
}

‎src/managers/conda/commands/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { CondaAvailableVersionsCommand } from './availableVersions';
2+
export { CondaCommandConstructorOptions } from './condaCommandOptions';
23
export { CondaInstallCommand } from './install';
34
export { CondaListCommand } from './list';
45
export { CondaUninstallCommand } from './uninstall';

‎src/managers/conda/commands/install.ts‎

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
1-
import { CommandConstructorOptions, InstallCommand, type InstallExecuteArgs } from '../../base/commands/index';
1+
import { InstallCommand, type InstallExecuteArgs } from '../../base/commands/index';
22
import { runCondaExecutable } from '../condaUtils';
3-
4-
/**
5-
* Conda install command execute arguments (includes the target environment path).
6-
*/
7-
export interface CondaInstallExecuteArgs extends InstallExecuteArgs {
8-
environmentPath: string;
9-
}
3+
import { CondaCommandConstructorOptions } from './condaCommandOptions';
104

115
/**
126
* Conda install command.
@@ -15,17 +9,15 @@ export interface CondaInstallExecuteArgs extends InstallExecuteArgs {
159
* Official documentation: https://conda.io/projects/conda/en/latest/commands/install.html
1610
*/
1711
export class CondaInstallCommand extends InstallCommand {
18-
constructor(options: CommandConstructorOptions) {
12+
private readonly condaEnvironmentPath: string;
13+
14+
constructor(options: CondaCommandConstructorOptions) {
1915
super(options);
16+
this.condaEnvironmentPath = options.condaEnvironmentPath;
2017
}
2118

2219
protected buildCommand(executeArgs: InstallExecuteArgs): string[] {
23-
const args = [executeArgs.upgrade ? 'update' : 'install', '-y'];
24-
25-
const { environmentPath } = executeArgs as CondaInstallExecuteArgs;
26-
if (environmentPath) {
27-
args.push('-p', environmentPath);
28-
}
20+
const args = [executeArgs.upgrade ? 'update' : 'install', '-y', '-p', this.condaEnvironmentPath];
2921

3022
args.push(
3123
...executeArgs.packages.map((pkg) => {

‎src/managers/conda/commands/list.ts‎

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,28 @@
11
import { PackageInfo } from '../../../api';
2-
import { CommandConstructorOptions, ListCommand, type BaseExecuteArgs } from '../../base/commands/index';
2+
import { ListCommand, type BaseExecuteArgs } from '../../base/commands/index';
33
import { runCondaExecutable } from '../condaUtils';
4-
5-
/**
6-
* Conda list command execute arguments (includes environment path and cancellation token).
7-
*/
8-
export interface CondaListExecuteArgs extends BaseExecuteArgs {
9-
environmentPath: string;
10-
}
4+
import { CondaCommandConstructorOptions } from './condaCommandOptions';
115

126
/**
137
* Conda list command.
148
* Parsed command: `conda list -p <environment_path> --json`
159
* Official documentation: https://conda.io/projects/conda/en/latest/commands/list.html
1610
*/
1711
export class CondaListCommand extends ListCommand {
18-
constructor(options: CommandConstructorOptions) {
12+
private readonly condaEnvironmentPath: string;
13+
14+
constructor(options: CondaCommandConstructorOptions) {
1915
super(options);
16+
this.condaEnvironmentPath = options.condaEnvironmentPath;
2017
}
2118

22-
protected buildCommand(executeArgs: CondaListExecuteArgs): string[] {
23-
return ['list', '-p', executeArgs.environmentPath, '--json'];
19+
protected buildCommand(): string[] {
20+
return ['list', '-p', this.condaEnvironmentPath, '--json'];
2421
}
2522

26-
async execute(executeArgs?: CondaListExecuteArgs): Promise<PackageInfo[]> {
27-
if (!executeArgs?.environmentPath) {
28-
return [];
29-
}
30-
31-
const cmdArgs = this.buildCommand(executeArgs);
32-
const output = await runCondaExecutable(cmdArgs, this.log, executeArgs.cancellationToken);
23+
async execute(executeArgs?: BaseExecuteArgs): Promise<PackageInfo[]> {
24+
const cmdArgs = this.buildCommand();
25+
const output = await runCondaExecutable(cmdArgs, this.log, executeArgs?.cancellationToken);
3326

3427
let condaPackages: { name: string; version: string }[];
3528
try {

0 commit comments

Comments
 (0)