Skip to content

Commit 379d4f7

Browse files
committed
pipenv pkg manager initial commit
1 parent 8f7710c commit 379d4f7

5 files changed

Lines changed: 228 additions & 53 deletions

File tree

src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
563563
registerSystemPythonFeatures(nativeFinder, context.subscriptions, outputChannel, sysMgr),
564564
registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel),
565565
registerPyenvFeatures(nativeFinder, context.subscriptions),
566-
registerPipenvFeatures(nativeFinder, context.subscriptions),
566+
registerPipenvFeatures(nativeFinder, context.subscriptions, outputChannel),
567567
registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel),
568568
shellStartupVarsMgr.initialize(),
569569
]);

src/managers/pipenv/main.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Disposable } from 'vscode';
1+
import { Disposable, LogOutputChannel } from 'vscode';
22
import { PythonEnvironmentApi } from '../../api';
33
import { traceInfo } from '../../common/logging';
44
import { getPythonApi } from '../../features/pythonApi';
@@ -10,6 +10,7 @@ import { getPipenv } from './pipenvUtils';
1010
export async function registerPipenvFeatures(
1111
nativeFinder: NativePythonFinder,
1212
disposables: Disposable[],
13+
log?: LogOutputChannel,
1314
): Promise<void> {
1415
const api: PythonEnvironmentApi = await getPythonApi();
1516

@@ -18,13 +19,13 @@ export async function registerPipenvFeatures(
1819

1920
if (pipenv) {
2021
const mgr = new PipenvManager(nativeFinder, api);
21-
const packageManager = new PipenvPackageManager(api);
22-
22+
const packageManager = new PipenvPackageManager(api, log);
23+
2324
disposables.push(
2425
mgr,
2526
packageManager,
2627
api.registerEnvironmentManager(mgr),
27-
api.registerPackageManager(packageManager)
28+
api.registerPackageManager(packageManager),
2829
);
2930
} else {
3031
traceInfo('Pipenv not found, turning off pipenv features.');

src/managers/pipenv/pipenvManager.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,10 @@ export class PipenvManager implements EnvironmentManager {
5252

5353
private _initialized: Deferred<void> | undefined;
5454

55-
constructor(
56-
public readonly nativeFinder: NativePythonFinder,
57-
public readonly api: PythonEnvironmentApi
58-
) {
55+
constructor(public readonly nativeFinder: NativePythonFinder, public readonly api: PythonEnvironmentApi) {
5956
this.name = 'pipenv';
6057
this.displayName = 'Pipenv';
61-
this.preferredPackageManagerId = 'ms-python.python:pipenv';
58+
this.preferredPackageManagerId = 'ms-python.python:pip';
6259
this.tooltip = new MarkdownString(PipenvStrings.pipenvManager, true);
6360
}
6461

@@ -114,9 +111,8 @@ export class PipenvManager implements EnvironmentManager {
114111
}
115112

116113
private findEnvironmentByPath(fsPath: string): PythonEnvironment | undefined {
117-
return this.collection.find((env) =>
118-
env.environmentPath.fsPath === fsPath ||
119-
env.execInfo?.run.executable === fsPath
114+
return this.collection.find(
115+
(env) => env.environmentPath.fsPath === fsPath || env.execInfo?.run.executable === fsPath,
120116
);
121117
}
122118

@@ -139,7 +135,7 @@ export class PipenvManager implements EnvironmentManager {
139135

140136
async refresh(scope: RefreshEnvironmentsScope): Promise<void> {
141137
const hardRefresh = scope === undefined; // hard refresh when scope is undefined
142-
138+
143139
await withProgress(
144140
{
145141
location: ProgressLocation.Window,
@@ -152,7 +148,7 @@ export class PipenvManager implements EnvironmentManager {
152148

153149
// Fire change events for environments that were added or removed
154150
const changes: { environment: PythonEnvironment; kind: EnvironmentChangeKind }[] = [];
155-
151+
156152
// Find removed environments
157153
oldCollection.forEach((oldEnv) => {
158154
if (!this.collection.find((newEnv) => newEnv.envId.id === oldEnv.envId.id)) {
@@ -203,7 +199,7 @@ export class PipenvManager implements EnvironmentManager {
203199
const before = this.globalEnv;
204200
this.globalEnv = environment;
205201
await setPipenvForGlobal(environment?.environmentPath.fsPath);
206-
202+
207203
if (before?.envId.id !== this.globalEnv?.envId.id) {
208204
this._onDidChangeEnvironment.fire({ uri: undefined, old: before, new: this.globalEnv });
209205
}
@@ -223,7 +219,7 @@ export class PipenvManager implements EnvironmentManager {
223219
} else {
224220
this.fsPathToEnv.delete(project.uri.fsPath);
225221
}
226-
222+
227223
await setPipenvForWorkspace(project.uri.fsPath, environment?.environmentPath.fsPath);
228224

229225
if (before?.envId.id !== environment?.envId.id) {
Lines changed: 145 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,185 @@
1-
import { EventEmitter, LogOutputChannel, MarkdownString } from 'vscode';
1+
import {
2+
CancellationError,
3+
CancellationToken,
4+
Event,
5+
EventEmitter,
6+
LogOutputChannel,
7+
MarkdownString,
8+
ProgressLocation,
9+
ThemeIcon,
10+
window,
11+
} from 'vscode';
12+
import { Disposable } from 'vscode-jsonrpc';
213
import {
314
DidChangePackagesEventArgs,
415
IconPath,
516
Package,
6-
PackageManager,
17+
PackageChangeKind,
718
PackageManagementOptions,
19+
PackageManager,
820
PythonEnvironment,
921
PythonEnvironmentApi,
1022
} from '../../api';
1123
import { traceInfo } from '../../common/logging';
24+
import { getPipenv, parsePipenvList, runPipenv } from './pipenvUtils';
1225

13-
export class PipenvPackageManager implements PackageManager {
26+
function getChanges(before: Package[], after: Package[]): { kind: PackageChangeKind; pkg: Package }[] {
27+
const changes: { kind: PackageChangeKind; pkg: Package }[] = [];
28+
before.forEach((pkg) => {
29+
changes.push({ kind: PackageChangeKind.remove, pkg });
30+
});
31+
after.forEach((pkg) => {
32+
changes.push({ kind: PackageChangeKind.add, pkg });
33+
});
34+
return changes;
35+
}
36+
37+
export class PipenvPackageManager implements PackageManager, Disposable {
1438
public readonly name: string;
1539
public readonly displayName?: string;
1640
public readonly description?: string;
1741
public readonly tooltip?: string | MarkdownString;
1842
public readonly iconPath?: IconPath;
19-
public readonly log?: LogOutputChannel;
2043

2144
private readonly _onDidChangePackages = new EventEmitter<DidChangePackagesEventArgs>();
22-
public readonly onDidChangePackages = this._onDidChangePackages.event;
45+
public readonly onDidChangePackages: Event<DidChangePackagesEventArgs> = this._onDidChangePackages.event;
46+
47+
private packages: Map<string, Package[]> = new Map();
2348

24-
constructor(
25-
public readonly api: PythonEnvironmentApi,
26-
log?: LogOutputChannel
27-
) {
49+
constructor(public readonly api: PythonEnvironmentApi, public readonly log?: LogOutputChannel) {
2850
this.name = 'pipenv';
2951
this.displayName = 'Pipenv';
3052
this.description = 'Manages packages using Pipenv';
3153
this.tooltip = new MarkdownString('Install and manage packages using Pipenv package manager');
32-
this.log = log;
54+
this.iconPath = new ThemeIcon('package');
3355
}
3456

3557
async manage(environment: PythonEnvironment, options: PackageManagementOptions): Promise<void> {
36-
// TODO: Implement pipenv package management
37-
// This would run commands like:
38-
// - pipenv install <package> for installation
39-
// - pipenv uninstall <package> for uninstallation
40-
// - pipenv install for installing from Pipfile
41-
42-
traceInfo(`Pipenv package management not yet implemented for environment: ${environment.name}`);
43-
traceInfo(`Options: ${JSON.stringify(options)}`);
44-
45-
// For now, just log the operation
46-
if (options.install && options.install.length > 0) {
47-
traceInfo(`Would install packages: ${options.install.join(', ')}`);
58+
const pipenvPath = await getPipenv();
59+
if (!pipenvPath) {
60+
throw new Error('Pipenv not found');
4861
}
62+
63+
let toInstall: string[] = [...(options.install ?? [])];
64+
let toUninstall: string[] = [...(options.uninstall ?? [])];
65+
66+
if (toInstall.length === 0 && toUninstall.length === 0) {
67+
traceInfo('No packages specified for installation or uninstallation');
68+
return;
69+
}
70+
71+
const manageOptions = {
72+
...options,
73+
install: toInstall,
74+
uninstall: toUninstall,
75+
};
76+
77+
await window.withProgress(
78+
{
79+
location: ProgressLocation.Notification,
80+
title: 'Managing packages with Pipenv',
81+
cancellable: true,
82+
},
83+
async (_progress, token) => {
84+
try {
85+
const before = this.packages.get(environment.envId.id) ?? [];
86+
const after = await this.managePipenvPackages(environment, manageOptions, pipenvPath, token);
87+
const changes = getChanges(before, after);
88+
this.packages.set(environment.envId.id, after);
89+
this._onDidChangePackages.fire({ environment, manager: this, changes });
90+
} catch (e) {
91+
if (e instanceof CancellationError) {
92+
throw e;
93+
}
94+
this.log?.error('Error managing packages', e);
95+
setImmediate(async () => {
96+
const result = await window.showErrorMessage(
97+
'Error managing packages with Pipenv',
98+
'View Output',
99+
);
100+
if (result === 'View Output') {
101+
this.log?.show();
102+
}
103+
});
104+
throw e;
105+
}
106+
},
107+
);
108+
}
109+
110+
private async managePipenvPackages(
111+
environment: PythonEnvironment,
112+
options: PackageManagementOptions,
113+
pipenvPath: string,
114+
token?: CancellationToken,
115+
): Promise<Package[]> {
116+
// Get the project directory from the environment's sysPrefix or a parent directory
117+
const projectDir = environment.sysPrefix;
118+
119+
// Uninstall packages first
49120
if (options.uninstall && options.uninstall.length > 0) {
50-
traceInfo(`Would uninstall packages: ${options.uninstall.join(', ')}`);
121+
const uninstallArgs = ['uninstall', ...options.uninstall, '--yes']; // Skip confirmation
122+
await runPipenv(pipenvPath, uninstallArgs, projectDir, this.log, token);
123+
}
124+
125+
// Install packages
126+
if (options.install && options.install.length > 0) {
127+
const installArgs = ['install', ...options.install];
128+
if (options.upgrade) {
129+
installArgs.push('--upgrade');
130+
}
131+
await runPipenv(pipenvPath, installArgs, projectDir, this.log, token);
51132
}
52133

53-
// Fire change event (though packages haven't actually changed)
54-
// this._onDidChangePackages.fire({ changes: [] });
134+
return await this.refreshPipenvPackages(environment, pipenvPath);
55135
}
56136

57137
async refresh(environment: PythonEnvironment): Promise<void> {
58-
// TODO: Implement package list refresh
59-
// This would run 'pipenv graph' or similar to get package list
60-
traceInfo(`Pipenv package refresh not yet implemented for environment: ${environment.name}`);
138+
await window.withProgress(
139+
{
140+
location: ProgressLocation.Window,
141+
title: 'Refreshing packages',
142+
},
143+
async () => {
144+
const pipenvPath = await getPipenv();
145+
if (!pipenvPath) {
146+
throw new Error('Pipenv not found');
147+
}
148+
149+
const before = this.packages.get(environment.envId.id) ?? [];
150+
const after = await this.refreshPipenvPackages(environment, pipenvPath);
151+
const changes = getChanges(before, after);
152+
this.packages.set(environment.envId.id, after);
153+
if (changes.length > 0) {
154+
this._onDidChangePackages.fire({ environment, manager: this, changes });
155+
}
156+
},
157+
);
158+
}
159+
160+
private async refreshPipenvPackages(environment: PythonEnvironment, pipenvPath: string): Promise<Package[]> {
161+
try {
162+
// Use pipenv run pip list to get packages in the virtual environment
163+
const projectDir = environment.sysPrefix;
164+
const data = await runPipenv(pipenvPath, ['run', 'pip', 'list'], projectDir, this.log);
165+
const pipenvPackages = parsePipenvList(data);
166+
167+
return pipenvPackages.map((pkg) => this.api.createPackageItem(pkg, environment, this));
168+
} catch (error) {
169+
this.log?.error('Error refreshing pipenv packages', error);
170+
return [];
171+
}
61172
}
62173

63174
async getPackages(environment: PythonEnvironment): Promise<Package[] | undefined> {
64-
// TODO: Implement package listing
65-
// This would parse output from 'pipenv graph' or 'pip list' in the pipenv environment
66-
traceInfo(`Pipenv package listing not yet implemented for environment: ${environment.name}`);
67-
return [];
175+
if (!this.packages.has(environment.envId.id)) {
176+
await this.refresh(environment);
177+
}
178+
return this.packages.get(environment.envId.id);
68179
}
69180

70181
public dispose() {
71182
this._onDidChangePackages.dispose();
183+
this.packages.clear();
72184
}
73-
}
185+
}

0 commit comments

Comments
 (0)