Skip to content

Commit 41ba9d1

Browse files
Copiloteleanorjboyd
andcommitted
Implement core Pipenv utilities and manager functionality
Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com>
1 parent 597b10c commit 41ba9d1

2 files changed

Lines changed: 408 additions & 24 deletions

File tree

src/managers/pipenv/pipenvManager.ts

Lines changed: 218 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,42 @@
1-
import { EventEmitter, MarkdownString } from 'vscode';
1+
import { EventEmitter, MarkdownString, ProgressLocation, Uri } from 'vscode';
22
import {
33
CreateEnvironmentOptions,
44
CreateEnvironmentScope,
55
DidChangeEnvironmentEventArgs,
66
DidChangeEnvironmentsEventArgs,
7+
EnvironmentChangeKind,
78
EnvironmentManager,
89
GetEnvironmentScope,
910
GetEnvironmentsScope,
1011
IconPath,
1112
PythonEnvironment,
1213
PythonEnvironmentApi,
14+
PythonProject,
1315
QuickCreateConfig,
1416
RefreshEnvironmentsScope,
1517
ResolveEnvironmentContext,
1618
SetEnvironmentScope,
1719
} from '../../api';
1820
import { PipenvStrings } from '../../common/localize';
21+
import { createDeferred, Deferred } from '../../common/utils/deferred';
22+
import { withProgress } from '../../common/window.apis';
1923
import { NativePythonFinder } from '../common/nativePythonFinder';
24+
import {
25+
clearPipenvCache,
26+
getPipenvForGlobal,
27+
getPipenvForWorkspace,
28+
refreshPipenv,
29+
resolvePipenvPath,
30+
setPipenvForGlobal,
31+
setPipenvForWorkspace,
32+
setPipenvForWorkspaces,
33+
} from './pipenvUtils';
2034

2135
export class PipenvManager implements EnvironmentManager {
36+
private collection: PythonEnvironment[] = [];
37+
private fsPathToEnv: Map<string, PythonEnvironment> = new Map();
38+
private globalEnv: PythonEnvironment | undefined;
39+
2240
private readonly _onDidChangeEnvironment = new EventEmitter<DidChangeEnvironmentEventArgs>();
2341
public readonly onDidChangeEnvironment = this._onDidChangeEnvironment.event;
2442

@@ -32,6 +50,8 @@ export class PipenvManager implements EnvironmentManager {
3250
public readonly tooltip: string | MarkdownString;
3351
public readonly iconPath?: IconPath;
3452

53+
private _initialized: Deferred<void> | undefined;
54+
3555
constructor(
3656
public readonly nativeFinder: NativePythonFinder,
3757
public readonly api: PythonEnvironmentApi
@@ -43,10 +63,63 @@ export class PipenvManager implements EnvironmentManager {
4363
}
4464

4565
public dispose() {
66+
this.collection = [];
67+
this.fsPathToEnv.clear();
4668
this._onDidChangeEnvironment.dispose();
4769
this._onDidChangeEnvironments.dispose();
4870
}
4971

72+
async initialize(): Promise<void> {
73+
if (this._initialized) {
74+
return this._initialized.promise;
75+
}
76+
77+
this._initialized = createDeferred();
78+
79+
await withProgress(
80+
{
81+
location: ProgressLocation.Window,
82+
title: PipenvStrings.pipenvDiscovering,
83+
},
84+
async () => {
85+
this.collection = await refreshPipenv(false, this.nativeFinder, this.api, this);
86+
await this.loadEnvMap();
87+
88+
this._onDidChangeEnvironments.fire(
89+
this.collection.map((e) => ({ environment: e, kind: EnvironmentChangeKind.add })),
90+
);
91+
},
92+
);
93+
this._initialized.resolve();
94+
}
95+
96+
private async loadEnvMap() {
97+
// Load environment mappings for projects
98+
const projects = this.api.getPythonProjects();
99+
for (const project of projects) {
100+
const envPath = await getPipenvForWorkspace(project.uri.fsPath);
101+
if (envPath) {
102+
const env = this.findEnvironmentByPath(envPath);
103+
if (env) {
104+
this.fsPathToEnv.set(project.uri.fsPath, env);
105+
}
106+
}
107+
}
108+
109+
// Load global environment
110+
const globalEnvPath = await getPipenvForGlobal();
111+
if (globalEnvPath) {
112+
this.globalEnv = this.findEnvironmentByPath(globalEnvPath);
113+
}
114+
}
115+
116+
private findEnvironmentByPath(fsPath: string): PythonEnvironment | undefined {
117+
return this.collection.find((env) =>
118+
env.environmentPath.fsPath === fsPath ||
119+
env.execInfo?.run.executable === fsPath
120+
);
121+
}
122+
50123
quickCreateConfig?(): QuickCreateConfig | undefined {
51124
// To be implemented
52125
return undefined;
@@ -64,34 +137,162 @@ export class PipenvManager implements EnvironmentManager {
64137
// To be implemented
65138
}
66139

67-
async refresh(_scope: RefreshEnvironmentsScope): Promise<void> {
68-
// TODO: Implement pipenv environment refresh
69-
// This should discover pipenv environments and update the collection
140+
async refresh(scope: RefreshEnvironmentsScope): Promise<void> {
141+
const hardRefresh = scope === undefined; // hard refresh when scope is undefined
142+
143+
await withProgress(
144+
{
145+
location: ProgressLocation.Window,
146+
title: PipenvStrings.pipenvRefreshing,
147+
},
148+
async () => {
149+
const oldCollection = [...this.collection];
150+
this.collection = await refreshPipenv(hardRefresh, this.nativeFinder, this.api, this);
151+
await this.loadEnvMap();
152+
153+
// Fire change events for environments that were added or removed
154+
const changes: { environment: PythonEnvironment; kind: EnvironmentChangeKind }[] = [];
155+
156+
// Find removed environments
157+
oldCollection.forEach((oldEnv) => {
158+
if (!this.collection.find((newEnv) => newEnv.envId.id === oldEnv.envId.id)) {
159+
changes.push({ environment: oldEnv, kind: EnvironmentChangeKind.remove });
160+
}
161+
});
162+
163+
// Find added environments
164+
this.collection.forEach((newEnv) => {
165+
if (!oldCollection.find((oldEnv) => oldEnv.envId.id === newEnv.envId.id)) {
166+
changes.push({ environment: newEnv, kind: EnvironmentChangeKind.add });
167+
}
168+
});
169+
170+
if (changes.length > 0) {
171+
this._onDidChangeEnvironments.fire(changes);
172+
}
173+
},
174+
);
70175
}
71176

72-
async getEnvironments(_scope: GetEnvironmentsScope): Promise<PythonEnvironment[]> {
73-
// TODO: Implement pipenv environment discovery
74-
// This should return all discovered pipenv environments
177+
async getEnvironments(scope: GetEnvironmentsScope): Promise<PythonEnvironment[]> {
178+
await this.initialize();
179+
180+
if (scope === 'all') {
181+
return Array.from(this.collection);
182+
}
183+
184+
if (scope === 'global') {
185+
// Return all environments for global scope
186+
return Array.from(this.collection);
187+
}
188+
189+
if (scope instanceof Uri) {
190+
const project = this.api.getPythonProject(scope);
191+
if (project) {
192+
const env = this.fsPathToEnv.get(project.uri.fsPath);
193+
return env ? [env] : [];
194+
}
195+
}
196+
75197
return [];
76198
}
77199

78-
async set(_scope: SetEnvironmentScope, _environment?: PythonEnvironment): Promise<void> {
79-
// TODO: Implement setting pipenv environment for a scope
80-
// This should update the selected environment for the given scope
200+
async set(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise<void> {
201+
if (scope === undefined) {
202+
// Global scope
203+
const before = this.globalEnv;
204+
this.globalEnv = environment;
205+
await setPipenvForGlobal(environment?.environmentPath.fsPath);
206+
207+
if (before?.envId.id !== this.globalEnv?.envId.id) {
208+
this._onDidChangeEnvironment.fire({ uri: undefined, old: before, new: this.globalEnv });
209+
}
210+
return;
211+
}
212+
213+
if (scope instanceof Uri) {
214+
// Single project scope
215+
const project = this.api.getPythonProject(scope);
216+
if (!project) {
217+
return;
218+
}
219+
220+
const before = this.fsPathToEnv.get(project.uri.fsPath);
221+
if (environment) {
222+
this.fsPathToEnv.set(project.uri.fsPath, environment);
223+
} else {
224+
this.fsPathToEnv.delete(project.uri.fsPath);
225+
}
226+
227+
await setPipenvForWorkspace(project.uri.fsPath, environment?.environmentPath.fsPath);
228+
229+
if (before?.envId.id !== environment?.envId.id) {
230+
this._onDidChangeEnvironment.fire({ uri: scope, old: before, new: environment });
231+
}
232+
}
233+
234+
if (Array.isArray(scope) && scope.every((u) => u instanceof Uri)) {
235+
// Multiple projects scope
236+
const projects: PythonProject[] = [];
237+
scope
238+
.map((s) => this.api.getPythonProject(s))
239+
.forEach((p) => {
240+
if (p) {
241+
projects.push(p);
242+
}
243+
});
244+
245+
const before: Map<string, PythonEnvironment | undefined> = new Map();
246+
projects.forEach((p) => {
247+
before.set(p.uri.fsPath, this.fsPathToEnv.get(p.uri.fsPath));
248+
if (environment) {
249+
this.fsPathToEnv.set(p.uri.fsPath, environment);
250+
} else {
251+
this.fsPathToEnv.delete(p.uri.fsPath);
252+
}
253+
});
254+
255+
await setPipenvForWorkspaces(
256+
projects.map((p) => p.uri.fsPath),
257+
environment?.environmentPath.fsPath,
258+
);
259+
260+
projects.forEach((p) => {
261+
const b = before.get(p.uri.fsPath);
262+
if (b?.envId.id !== environment?.envId.id) {
263+
this._onDidChangeEnvironment.fire({ uri: p.uri, old: b, new: environment });
264+
}
265+
});
266+
}
81267
}
82268

83-
async get(_scope: GetEnvironmentScope): Promise<PythonEnvironment | undefined> {
84-
// TODO: Implement getting the selected pipenv environment for a scope
269+
async get(scope: GetEnvironmentScope): Promise<PythonEnvironment | undefined> {
270+
await this.initialize();
271+
272+
if (scope === undefined) {
273+
return this.globalEnv;
274+
}
275+
276+
if (scope instanceof Uri) {
277+
const project = this.api.getPythonProject(scope);
278+
if (project) {
279+
return this.fsPathToEnv.get(project.uri.fsPath);
280+
}
281+
}
282+
85283
return undefined;
86284
}
87285

88-
async resolve(_context: ResolveEnvironmentContext): Promise<PythonEnvironment | undefined> {
89-
// TODO: Implement resolving a path to a pipenv environment
90-
return undefined;
286+
async resolve(context: ResolveEnvironmentContext): Promise<PythonEnvironment | undefined> {
287+
await this.initialize();
288+
return resolvePipenvPath(context.fsPath, this.nativeFinder, this.api, this);
91289
}
92290

93291
async clearCache?(): Promise<void> {
94-
// TODO: Implement cache clearing
95-
// This should clear any cached environment discovery data
292+
await clearPipenvCache();
293+
this.collection = [];
294+
this.fsPathToEnv.clear();
295+
this.globalEnv = undefined;
296+
this._initialized = undefined;
96297
}
97298
}

0 commit comments

Comments
 (0)