Skip to content

Commit d6fcd29

Browse files
Adding pipenv support (#750)
fixes #686 --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]>
1 parent 57f85dc commit d6fcd29

File tree

5 files changed

+562
-0
lines changed

5 files changed

+562
-0
lines changed

src/common/localize.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,12 @@ export namespace PyenvStrings {
156156
export const pyenvRefreshing = l10n.t('Refreshing Pyenv Python versions');
157157
}
158158

159+
export namespace PipenvStrings {
160+
export const pipenvManager = l10n.t('Manages Pipenv environments');
161+
export const pipenvDiscovering = l10n.t('Discovering Pipenv environments');
162+
export const pipenvRefreshing = l10n.t('Refreshing Pipenv environments');
163+
}
164+
159165
export namespace PoetryStrings {
160166
export const poetryManager = l10n.t('Manages Poetry environments');
161167
export const poetryDiscovering = l10n.t('Discovering Poetry environments');

src/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ import {
7676
} from './managers/common/nativePythonFinder';
7777
import { IDisposable } from './managers/common/types';
7878
import { registerCondaFeatures } from './managers/conda/main';
79+
import { registerPipenvFeatures } from './managers/pipenv/main';
7980
import { registerPoetryFeatures } from './managers/poetry/main';
8081
import { registerPyenvFeatures } from './managers/pyenv/main';
8182

@@ -562,6 +563,7 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
562563
registerSystemPythonFeatures(nativeFinder, context.subscriptions, outputChannel, sysMgr),
563564
registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel),
564565
registerPyenvFeatures(nativeFinder, context.subscriptions),
566+
registerPipenvFeatures(nativeFinder, context.subscriptions),
565567
registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel),
566568
shellStartupVarsMgr.initialize(),
567569
]);

src/managers/pipenv/main.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Disposable } from 'vscode';
2+
import { PythonEnvironmentApi } from '../../api';
3+
import { traceInfo } from '../../common/logging';
4+
import { getPythonApi } from '../../features/pythonApi';
5+
import { NativePythonFinder } from '../common/nativePythonFinder';
6+
import { PipenvManager } from './pipenvManager';
7+
import { getPipenv } from './pipenvUtils';
8+
9+
export async function registerPipenvFeatures(
10+
nativeFinder: NativePythonFinder,
11+
disposables: Disposable[],
12+
): Promise<void> {
13+
const api: PythonEnvironmentApi = await getPythonApi();
14+
15+
try {
16+
const pipenv = await getPipenv(nativeFinder);
17+
18+
if (pipenv) {
19+
const mgr = new PipenvManager(nativeFinder, api);
20+
21+
disposables.push(mgr, api.registerEnvironmentManager(mgr));
22+
} else {
23+
traceInfo('Pipenv not found, turning off pipenv features.');
24+
}
25+
} catch (ex) {
26+
traceInfo('Pipenv not found, turning off pipenv features.', ex);
27+
}
28+
}
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
import { EventEmitter, MarkdownString, ProgressLocation, Uri } from 'vscode';
2+
import {
3+
CreateEnvironmentOptions,
4+
CreateEnvironmentScope,
5+
DidChangeEnvironmentEventArgs,
6+
DidChangeEnvironmentsEventArgs,
7+
EnvironmentChangeKind,
8+
EnvironmentManager,
9+
GetEnvironmentScope,
10+
GetEnvironmentsScope,
11+
IconPath,
12+
PythonEnvironment,
13+
PythonEnvironmentApi,
14+
PythonProject,
15+
RefreshEnvironmentsScope,
16+
ResolveEnvironmentContext,
17+
SetEnvironmentScope,
18+
} from '../../api';
19+
import { PipenvStrings } from '../../common/localize';
20+
import { createDeferred, Deferred } from '../../common/utils/deferred';
21+
import { withProgress } from '../../common/window.apis';
22+
import { NativePythonFinder } from '../common/nativePythonFinder';
23+
import {
24+
clearPipenvCache,
25+
getPipenvForGlobal,
26+
getPipenvForWorkspace,
27+
refreshPipenv,
28+
resolvePipenvPath,
29+
setPipenvForGlobal,
30+
setPipenvForWorkspace,
31+
setPipenvForWorkspaces,
32+
} from './pipenvUtils';
33+
34+
export class PipenvManager implements EnvironmentManager {
35+
private collection: PythonEnvironment[] = [];
36+
private fsPathToEnv: Map<string, PythonEnvironment> = new Map();
37+
private globalEnv: PythonEnvironment | undefined;
38+
39+
private readonly _onDidChangeEnvironment = new EventEmitter<DidChangeEnvironmentEventArgs>();
40+
public readonly onDidChangeEnvironment = this._onDidChangeEnvironment.event;
41+
42+
private readonly _onDidChangeEnvironments = new EventEmitter<DidChangeEnvironmentsEventArgs>();
43+
public readonly onDidChangeEnvironments = this._onDidChangeEnvironments.event;
44+
45+
public readonly name: string;
46+
public readonly displayName: string;
47+
public readonly preferredPackageManagerId: string;
48+
public readonly description?: string;
49+
public readonly tooltip: string | MarkdownString;
50+
public readonly iconPath?: IconPath;
51+
52+
private _initialized: Deferred<void> | undefined;
53+
54+
constructor(public readonly nativeFinder: NativePythonFinder, public readonly api: PythonEnvironmentApi) {
55+
this.name = 'pipenv';
56+
this.displayName = 'Pipenv';
57+
this.preferredPackageManagerId = 'ms-python.python:pip';
58+
this.tooltip = new MarkdownString(PipenvStrings.pipenvManager, true);
59+
}
60+
61+
public dispose() {
62+
this.collection = [];
63+
this.fsPathToEnv.clear();
64+
this._onDidChangeEnvironment.dispose();
65+
this._onDidChangeEnvironments.dispose();
66+
}
67+
68+
async initialize(): Promise<void> {
69+
if (this._initialized) {
70+
return this._initialized.promise;
71+
}
72+
73+
this._initialized = createDeferred();
74+
75+
await withProgress(
76+
{
77+
location: ProgressLocation.Window,
78+
title: PipenvStrings.pipenvDiscovering,
79+
},
80+
async () => {
81+
this.collection = await refreshPipenv(false, this.nativeFinder, this.api, this);
82+
await this.loadEnvMap();
83+
84+
this._onDidChangeEnvironments.fire(
85+
this.collection.map((e) => ({ environment: e, kind: EnvironmentChangeKind.add })),
86+
);
87+
},
88+
);
89+
this._initialized.resolve();
90+
}
91+
92+
private async loadEnvMap() {
93+
// Load environment mappings for projects
94+
const projects = this.api.getPythonProjects();
95+
for (const project of projects) {
96+
const envPath = await getPipenvForWorkspace(project.uri.fsPath);
97+
if (envPath) {
98+
const env = this.findEnvironmentByPath(envPath);
99+
if (env) {
100+
this.fsPathToEnv.set(project.uri.fsPath, env);
101+
}
102+
}
103+
}
104+
105+
// Load global environment
106+
const globalEnvPath = await getPipenvForGlobal();
107+
if (globalEnvPath) {
108+
this.globalEnv = this.findEnvironmentByPath(globalEnvPath);
109+
}
110+
}
111+
112+
private findEnvironmentByPath(fsPath: string): PythonEnvironment | undefined {
113+
return this.collection.find(
114+
(env) => env.environmentPath.fsPath === fsPath || env.execInfo?.run.executable === fsPath,
115+
);
116+
}
117+
118+
async create?(
119+
_scope: CreateEnvironmentScope,
120+
_options?: CreateEnvironmentOptions,
121+
): Promise<PythonEnvironment | undefined> {
122+
// To be implemented
123+
return undefined;
124+
}
125+
126+
async refresh(scope: RefreshEnvironmentsScope): Promise<void> {
127+
const hardRefresh = scope === undefined; // hard refresh when scope is undefined
128+
129+
await withProgress(
130+
{
131+
location: ProgressLocation.Window,
132+
title: PipenvStrings.pipenvRefreshing,
133+
},
134+
async () => {
135+
const oldCollection = [...this.collection];
136+
this.collection = await refreshPipenv(hardRefresh, this.nativeFinder, this.api, this);
137+
await this.loadEnvMap();
138+
139+
// Fire change events for environments that were added or removed
140+
const changes: { environment: PythonEnvironment; kind: EnvironmentChangeKind }[] = [];
141+
142+
// Find removed environments
143+
oldCollection.forEach((oldEnv) => {
144+
if (!this.collection.find((newEnv) => newEnv.envId.id === oldEnv.envId.id)) {
145+
changes.push({ environment: oldEnv, kind: EnvironmentChangeKind.remove });
146+
}
147+
});
148+
149+
// Find added environments
150+
this.collection.forEach((newEnv) => {
151+
if (!oldCollection.find((oldEnv) => oldEnv.envId.id === newEnv.envId.id)) {
152+
changes.push({ environment: newEnv, kind: EnvironmentChangeKind.add });
153+
}
154+
});
155+
156+
if (changes.length > 0) {
157+
this._onDidChangeEnvironments.fire(changes);
158+
}
159+
},
160+
);
161+
}
162+
163+
async getEnvironments(scope: GetEnvironmentsScope): Promise<PythonEnvironment[]> {
164+
await this.initialize();
165+
166+
if (scope === 'all') {
167+
return Array.from(this.collection);
168+
}
169+
170+
if (scope === 'global') {
171+
// Return all environments for global scope
172+
return Array.from(this.collection);
173+
}
174+
175+
if (scope instanceof Uri) {
176+
const project = this.api.getPythonProject(scope);
177+
if (project) {
178+
const env = this.fsPathToEnv.get(project.uri.fsPath);
179+
return env ? [env] : [];
180+
}
181+
}
182+
183+
return [];
184+
}
185+
186+
async set(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise<void> {
187+
if (scope === undefined) {
188+
// Global scope
189+
const before = this.globalEnv;
190+
this.globalEnv = environment;
191+
await setPipenvForGlobal(environment?.environmentPath.fsPath);
192+
193+
if (before?.envId.id !== this.globalEnv?.envId.id) {
194+
this._onDidChangeEnvironment.fire({ uri: undefined, old: before, new: this.globalEnv });
195+
}
196+
return;
197+
}
198+
199+
if (scope instanceof Uri) {
200+
// Single project scope
201+
const project = this.api.getPythonProject(scope);
202+
if (!project) {
203+
return;
204+
}
205+
206+
const before = this.fsPathToEnv.get(project.uri.fsPath);
207+
if (environment) {
208+
this.fsPathToEnv.set(project.uri.fsPath, environment);
209+
} else {
210+
this.fsPathToEnv.delete(project.uri.fsPath);
211+
}
212+
213+
await setPipenvForWorkspace(project.uri.fsPath, environment?.environmentPath.fsPath);
214+
215+
if (before?.envId.id !== environment?.envId.id) {
216+
this._onDidChangeEnvironment.fire({ uri: scope, old: before, new: environment });
217+
}
218+
}
219+
220+
if (Array.isArray(scope) && scope.every((u) => u instanceof Uri)) {
221+
// Multiple projects scope
222+
const projects: PythonProject[] = [];
223+
scope
224+
.map((s) => this.api.getPythonProject(s))
225+
.forEach((p) => {
226+
if (p) {
227+
projects.push(p);
228+
}
229+
});
230+
231+
const before: Map<string, PythonEnvironment | undefined> = new Map();
232+
projects.forEach((p) => {
233+
before.set(p.uri.fsPath, this.fsPathToEnv.get(p.uri.fsPath));
234+
if (environment) {
235+
this.fsPathToEnv.set(p.uri.fsPath, environment);
236+
} else {
237+
this.fsPathToEnv.delete(p.uri.fsPath);
238+
}
239+
});
240+
241+
await setPipenvForWorkspaces(
242+
projects.map((p) => p.uri.fsPath),
243+
environment?.environmentPath.fsPath,
244+
);
245+
246+
projects.forEach((p) => {
247+
const b = before.get(p.uri.fsPath);
248+
if (b?.envId.id !== environment?.envId.id) {
249+
this._onDidChangeEnvironment.fire({ uri: p.uri, old: b, new: environment });
250+
}
251+
});
252+
}
253+
}
254+
255+
async get(scope: GetEnvironmentScope): Promise<PythonEnvironment | undefined> {
256+
await this.initialize();
257+
258+
if (scope === undefined) {
259+
return this.globalEnv;
260+
}
261+
262+
if (scope instanceof Uri) {
263+
const project = this.api.getPythonProject(scope);
264+
if (project) {
265+
return this.fsPathToEnv.get(project.uri.fsPath);
266+
}
267+
}
268+
269+
return undefined;
270+
}
271+
272+
async resolve(context: ResolveEnvironmentContext): Promise<PythonEnvironment | undefined> {
273+
await this.initialize();
274+
return resolvePipenvPath(context.fsPath, this.nativeFinder, this.api, this);
275+
}
276+
277+
async clearCache?(): Promise<void> {
278+
await clearPipenvCache();
279+
this.collection = [];
280+
this.fsPathToEnv.clear();
281+
this.globalEnv = undefined;
282+
this._initialized = undefined;
283+
}
284+
}

0 commit comments

Comments
 (0)