Skip to content

Commit 54b13fe

Browse files
committed
Add package manager command classes
1 parent 4ff432c commit 54b13fe

30 files changed

Lines changed: 945 additions & 0 deletions
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { Pep440Version } from '@renovatebot/pep440';
2+
import { explain as parsePep440Version } from '@renovatebot/pep440';
3+
import { BaseExecuteArgs, PackageManagerCommand } from './packageManagerCommand';
4+
5+
/**
6+
* Arguments for available versions command execution (change per execution).
7+
*/
8+
export interface AvailableVersionsExecuteArgs extends BaseExecuteArgs {
9+
packageName: string;
10+
pythonVersion: string;
11+
includePrerelease?: boolean;
12+
}
13+
14+
/**
15+
* Template class for availableVersions commands.
16+
* Subclasses implement concrete package-manager-specific logic.
17+
*/
18+
export abstract class AvailableVersionsCommand extends PackageManagerCommand {
19+
protected static readonly configSection = 'availableVersionsCommandArgs';
20+
protected abstract buildCommand(executeArgs: AvailableVersionsExecuteArgs): string[];
21+
protected parseVersions(versions: string[], includePrerelease?: boolean): Pep440Version[] {
22+
let parsed = Array.from(new Set(versions))
23+
.map((version) => parsePep440Version(version.trim()))
24+
.filter((version): version is Pep440Version => version !== null);
25+
if (includePrerelease === false) {
26+
parsed = parsed.filter((version) => !/[ab]|rc|dev/i.test(version.public));
27+
}
28+
return parsed;
29+
}
30+
abstract execute(executeArgs: AvailableVersionsExecuteArgs): Promise<Pep440Version[]>;
31+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export { AvailableVersionsCommand, type AvailableVersionsExecuteArgs } from './availableVersions';
2+
export { InstallCommand, type InstallExecuteArgs } from './install';
3+
export { ListCommand } from './list';
4+
export { ListDirectNamesCommand } from './listDirectNames';
5+
export { BaseExecuteArgs, CommandConstructorOptions, PackageManagerCommand } from './packageManagerCommand';
6+
export { UninstallCommand, type UninstallExecuteArgs } from './uninstall';
7+
export { VersionCommand } from './version';
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { BaseExecuteArgs, PackageManagerCommand } from './packageManagerCommand';
2+
3+
/**
4+
* Arguments for install command execution (change per execution).
5+
*/
6+
export interface InstallExecuteArgs extends BaseExecuteArgs {
7+
packages: { packageName: string; version?: string }[];
8+
upgrade?: boolean;
9+
}
10+
11+
/**
12+
* Template class for install commands.
13+
* Subclasses implement concrete package-manager-specific logic.
14+
*/
15+
export abstract class InstallCommand extends PackageManagerCommand {
16+
protected static readonly configSection = 'installCommandArgs';
17+
protected abstract buildCommand(executeArgs: InstallExecuteArgs): string[];
18+
abstract execute(executeArgs: InstallExecuteArgs): Promise<void>;
19+
}

src/managers/base/commands/list.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { PackageInfo } from '../../../api';
2+
import { BaseExecuteArgs, PackageManagerCommand } from './packageManagerCommand';
3+
4+
/**
5+
* Template class for list commands.
6+
* Subclasses implement concrete package-manager-specific logic.
7+
*/
8+
export abstract class ListCommand extends PackageManagerCommand {
9+
protected static readonly configSection = 'listCommandArgs';
10+
protected timeout = 30000;
11+
abstract execute(executeArgs?: BaseExecuteArgs): Promise<PackageInfo[]>;
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { BaseExecuteArgs, PackageManagerCommand } from './packageManagerCommand';
2+
3+
/**
4+
* Template class for listDirectNames commands.
5+
* Subclasses implement concrete package-manager-specific logic.
6+
*/
7+
export abstract class ListDirectNamesCommand extends PackageManagerCommand {
8+
protected static readonly configSection = 'listDirectNamesCommandArgs';
9+
protected timeout = 30000;
10+
abstract execute(executeArgs?: BaseExecuteArgs): Promise<Set<string>>;
11+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { CancellationToken, LogOutputChannel, WorkspaceConfiguration } from 'vscode';
2+
import { getConfiguration } from '../../../common/workspace.apis';
3+
4+
/**
5+
* Base interface for all command execute arguments.
6+
* Provides optional cancellation token that all commands can use.
7+
*/
8+
export interface BaseExecuteArgs {
9+
cancellationToken?: CancellationToken;
10+
}
11+
12+
/**
13+
* Constructor options shared by all package manager commands.
14+
*/
15+
export interface CommandConstructorOptions {
16+
pythonExecutable: string;
17+
cwd?: string;
18+
log?: LogOutputChannel;
19+
}
20+
21+
/**
22+
* Base class for all package manager commands.
23+
* Provides common properties and minimal interface for subclasses.
24+
*/
25+
export abstract class PackageManagerCommand {
26+
protected static readonly configSection?: string;
27+
28+
protected pythonExecutable: string;
29+
protected cwd?: string;
30+
protected log?: LogOutputChannel;
31+
protected timeout: number = 300000;
32+
protected config?: WorkspaceConfiguration;
33+
34+
constructor(options: CommandConstructorOptions) {
35+
this.pythonExecutable = options.pythonExecutable;
36+
this.cwd = options.cwd;
37+
this.log = options.log;
38+
const configSection = (this.constructor as typeof PackageManagerCommand).configSection;
39+
this.config = configSection ? getConfiguration(`python-envs.packageManager.${configSection}`) : undefined;
40+
}
41+
42+
/**
43+
* Subclasses implement to build the command arguments.
44+
*/
45+
protected abstract buildCommand(executeArgs: BaseExecuteArgs): string[];
46+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { BaseExecuteArgs, PackageManagerCommand } from './packageManagerCommand';
2+
3+
/**
4+
* Arguments for uninstall command execution (change per execution).
5+
*/
6+
export interface UninstallExecuteArgs extends BaseExecuteArgs {
7+
packages: { packageName: string; version?: string }[];
8+
}
9+
10+
/**
11+
* Template class for uninstall commands.
12+
* Subclasses implement concrete package-manager-specific logic.
13+
*/
14+
export abstract class UninstallCommand extends PackageManagerCommand {
15+
protected static readonly configSection = 'uninstallCommandArgs';
16+
protected abstract buildCommand(executeArgs: UninstallExecuteArgs): string[];
17+
abstract execute(executeArgs: UninstallExecuteArgs): Promise<void>;
18+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { Pep440Version } from '@renovatebot/pep440';
2+
import { BaseExecuteArgs, PackageManagerCommand } from './packageManagerCommand';
3+
4+
/**
5+
* Template class for version commands.
6+
* Subclasses implement concrete package-manager-specific logic.
7+
*/
8+
export abstract class VersionCommand extends PackageManagerCommand {
9+
protected static readonly configSection = 'versionCommandArgs';
10+
abstract execute(executeArgs?: BaseExecuteArgs): Promise<Pep440Version | undefined>;
11+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import type { Pep440Version } from '@renovatebot/pep440';
2+
import { AvailableVersionsCommand, type AvailableVersionsExecuteArgs } from '../../base/commands/index';
3+
import { runPython, runUV } from '../helpers';
4+
5+
/**
6+
* Pip available versions command.
7+
* Parsed command: `python -m pip index versions <package> --json --python-version <version>`
8+
* Official documentation: https://pip.pypa.io/en/stable/cli/pip_index/
9+
*/
10+
export class PipAvailableVersionsCommand extends AvailableVersionsCommand {
11+
protected buildCommand(executeArgs: AvailableVersionsExecuteArgs): string[] {
12+
return [
13+
'-m',
14+
'pip',
15+
'index',
16+
'versions',
17+
executeArgs.packageName,
18+
'--json',
19+
'--python-version',
20+
executeArgs.pythonVersion,
21+
];
22+
}
23+
24+
async execute(executeArgs: AvailableVersionsExecuteArgs): Promise<Pep440Version[]> {
25+
const output = await runPython(
26+
this.pythonExecutable,
27+
this.buildCommand(executeArgs),
28+
undefined,
29+
this.log,
30+
executeArgs.cancellationToken,
31+
this.timeout,
32+
);
33+
const match = output.match(/{[\s\S]*}/);
34+
if (!match) {
35+
return [];
36+
}
37+
38+
try {
39+
const parsed = JSON.parse(match[0]) as { versions?: string[] };
40+
return this.parseVersions(
41+
Array.isArray(parsed.versions) ? parsed.versions : [],
42+
executeArgs.includePrerelease,
43+
);
44+
} catch {
45+
return [];
46+
}
47+
}
48+
}
49+
50+
/**
51+
* UV available versions command.
52+
* Parsed command: `uv tool run pip index versions <package> --json --python-version <version>`
53+
* Official documentation: https://docs.astral.sh/uv/pip/
54+
*/
55+
export class UvAvailableVersionsCommand extends AvailableVersionsCommand {
56+
protected buildCommand(executeArgs: AvailableVersionsExecuteArgs): string[] {
57+
return [
58+
'tool',
59+
'run',
60+
'pip',
61+
'index',
62+
'versions',
63+
executeArgs.packageName,
64+
'--json',
65+
'--python-version',
66+
executeArgs.pythonVersion,
67+
];
68+
}
69+
70+
async execute(executeArgs: AvailableVersionsExecuteArgs): Promise<Pep440Version[]> {
71+
const output = await runUV(
72+
this.buildCommand(executeArgs),
73+
undefined,
74+
this.log,
75+
executeArgs.cancellationToken,
76+
this.timeout,
77+
);
78+
const match = output.match(/{[\s\S]*}/);
79+
if (!match) {
80+
return [];
81+
}
82+
83+
try {
84+
const parsed = JSON.parse(match[0]) as { versions?: string[] };
85+
return this.parseVersions(
86+
Array.isArray(parsed.versions) ? parsed.versions : [],
87+
executeArgs.includePrerelease,
88+
);
89+
} catch {
90+
return [];
91+
}
92+
}
93+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { CommandConstructorOptions } from '../../base/commands/index';
2+
import { shouldUseUv } from '../helpers';
3+
4+
type CommandConstructor<T> = new (options: CommandConstructorOptions) => T;
5+
6+
export async function createPipOrUvCommand<T, P extends T, U extends T>(
7+
options: CommandConstructorOptions,
8+
environmentPath: string,
9+
PipCommand: CommandConstructor<P>,
10+
UvCommand: CommandConstructor<U>,
11+
): Promise<T> {
12+
return (await shouldUseUv(options.log, environmentPath)) ? new UvCommand(options) : new PipCommand(options);
13+
}

0 commit comments

Comments
 (0)