Skip to content

Commit 338bbb0

Browse files
edvilmeCopilot
andcommitted
feat: add project-scoped package managers
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ab7e086 commit 338bbb0

17 files changed

Lines changed: 403 additions & 82 deletions

‎api/CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to the `@vscode/python-environments` API package are documen
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.5.0]
9+
10+
### Added
11+
12+
- Added the optional `PackageManager.createForProject` factory for package managers whose operations depend on the calling Python project.
13+
814
## [1.4.0]
915

1016
### Changed

‎api/package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎api/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@vscode/python-environments",
33
"description": "An API facade for the Python Environments extension in VS Code",
4-
"version": "1.4.0",
4+
"version": "1.5.0",
55
"author": {
66
"name": "Microsoft Corporation"
77
},

‎docs/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1613,6 +1613,7 @@ Reports and changes the packages of an environment.
16131613
| `refresh(environment)` | `(environment: PythonEnvironment) => Promise<void>` | Yes | Re-reads the installed package list. |
16141614
| `getPackages(environment, options?)` | `(environment: PythonEnvironment, options?: GetPackagesOptions) => Promise<Package[] \| undefined>` | Yes | Returns installed packages, or `undefined` if they cannot be retrieved. |
16151615
| `getPackageWatchTargets(environment)` | `(environment: PythonEnvironment) => RelativePattern[]` | No | Extra filesystem patterns to watch for install and uninstall changes, appended to the default site-packages locations. Implement for manager-specific locations such as `conda-meta`. |
1616+
| `createForProject(project)` | `(project: PythonProject) => PackageManager` | No | Creates a manager bound to a project for project-sensitive operations. |
16161617
| `getDirectPackageNames(environment)` | `(environment: PythonEnvironment) => Promise<Set<string> \| undefined>` | No | Best-effort set of non-transitive package names. Most tools cannot record user intent - pip uses `pip list --not-required`, which reports leaf packages rather than explicitly installed ones. |
16171618
| `clearCache()` | `() => Promise<void>` | No | Drops cached package data. |
16181619
| `getVersion(environment)` | `(environment: PythonEnvironment) => Promise<Pep440Version \| undefined>` | No | Version of the underlying tool, such as pip, uv, or conda. |

‎src/extensionApi.ts‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
8686
this._onDidChangePythonProjects,
8787
this._onDidChangePackages,
8888
this._onDidChangeEnvironmentVariables,
89+
this.envManagers.onDidChangePackageProviderPackages((e) => this._onDidChangePackages.fire(e)),
8990
this.envManagers.onDidChangeActiveEnvironment((e) => {
9091
this._onDidChangeEnvironment.fire(e);
9192
const location = e.uri?.fsPath ?? 'global';
@@ -295,12 +296,7 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
295296
}
296297

297298
registerPackageManager(manager: PackageManager, options?: { extensionId?: string }): Disposable {
298-
const disposables: Disposable[] = [];
299-
disposables.push(this.envManagers.registerPackageManager(manager, options));
300-
if (manager.onDidChangePackages) {
301-
disposables.push(manager.onDidChangePackages((e) => this._onDidChangePackages.fire(e)));
302-
}
303-
return new Disposable(() => disposables.forEach((d) => d.dispose()));
299+
return this.envManagers.registerPackageManager(manager, options);
304300
}
305301
async managePackages(context: PythonEnvironment, options: PackageManagementOptions): Promise<void> {
306302
await waitForEnvManagerId([context.envId.managerId]);

‎src/features/envCommands.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,8 @@ export async function handlePackageUninstall(context: unknown, em: EnvironmentMa
343343
}
344344
const moduleName = context.pkg.name;
345345
const environment = context.parent.environment;
346-
const packageManager = em.getPackageManager(environment);
346+
const packageManager =
347+
context instanceof ProjectPackage ? context.manager : em.getPackageManager(environment);
347348
await packageManager?.manage(environment, { uninstall: [moduleName], install: [] });
348349
return;
349350
}
@@ -358,7 +359,8 @@ export async function managePackageVersion(context: unknown, em: EnvironmentMana
358359
if (context instanceof PackageTreeItem || context instanceof ProjectPackage) {
359360
const pkg = context.pkg;
360361
const environment = context.parent.environment;
361-
const packageManager = em.getPackageManager(environment);
362+
const packageManager =
363+
context instanceof ProjectPackage ? context.manager : em.getPackageManager(environment);
362364

363365
if (!packageManager) {
364366
return;

‎src/features/envManagers.ts‎

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export interface EnvironmentManagers extends Disposable {
111111
*/
112112
onDidChangeActiveEnvironment: Event<DidChangeEnvironmentEventArgs>;
113113
onDidChangePackages: Event<InternalDidChangePackagesEventArgs>;
114+
onDidChangePackageProviderPackages: Event<DidChangePackagesEventArgs>;
114115

115116
onDidChangeEnvironmentManager: Event<DidChangeEnvironmentManagerEventArgs>;
116117
onDidChangePackageManager: Event<DidChangePackageManagerEventArgs>;
@@ -171,6 +172,11 @@ function generateId(name: string, extensionId?: string): string {
171172
export class PythonEnvironmentManagers implements EnvironmentManagers {
172173
private _environmentManagers: Map<string, InternalEnvironmentManager> = new Map();
173174
private _packageManagers: Map<string, InternalPackageManager> = new Map();
175+
private _projectPackageManagers: Map<string, InternalPackageManager> = new Map();
176+
private readonly _packageManagerEventSubscriptions = new Map<
177+
string,
178+
Map<Event<DidChangePackagesEventArgs>, Disposable>
179+
>();
174180
private readonly subscriptions: Disposable[] = [];
175181

176182
/**
@@ -198,6 +204,7 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
198204
/** Fires when the active (selected) environment for a scope actually changes. */
199205
private _onDidChangeActiveEnvironment = new EventEmitter<DidChangeEnvironmentEventArgs>();
200206
private _onDidChangePackages = new EventEmitter<InternalDidChangePackagesEventArgs>();
207+
private _onDidChangePackageProviderPackages = new EventEmitter<DidChangePackagesEventArgs>();
201208

202209
public onDidChangeEnvironmentManager: Event<DidChangeEnvironmentManagerEventArgs> =
203210
this._onDidChangeEnvironmentManager.event;
@@ -208,6 +215,8 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
208215
public onDidChangeManagerEnvironment: Event<DidChangeEnvironmentEventArgs> =
209216
this._onDidChangeManagerEnvironment.event;
210217
public onDidChangePackages: Event<InternalDidChangePackagesEventArgs> = this._onDidChangePackages.event;
218+
public onDidChangePackageProviderPackages: Event<DidChangePackagesEventArgs> =
219+
this._onDidChangePackageProviderPackages.event;
211220

212221
/** Fires only when the *selected* manager's environment for a scope actually changes. */
213222
public onDidChangeActiveEnvironment: Event<DidChangeEnvironmentEventArgs> =
@@ -301,20 +310,9 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
301310
traceError(ex);
302311
throw ex;
303312
}
304-
const disposables: Disposable[] = [];
305313
const mgr = new InternalPackageManager(managerId, manager);
306314

307-
disposables.push(
308-
mgr.onDidChangePackages((e: DidChangePackagesEventArgs) => {
309-
setImmediate(() =>
310-
this._onDidChangePackages.fire({
311-
environment: e.environment,
312-
manager: mgr,
313-
changes: e.changes,
314-
}),
315-
);
316-
}),
317-
);
315+
this.subscribeToPackageManagerEvents(mgr, mgr);
318316

319317
this._packageManagers.set(managerId, mgr);
320318
this._onDidChangePackageManager.fire({ kind: 'registered', manager: mgr });
@@ -327,14 +325,23 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
327325

328326
return new Disposable(() => {
329327
this._packageManagers.delete(managerId);
330-
disposables.forEach((d) => d.dispose());
328+
for (const [key, scopedManager] of this._projectPackageManagers) {
329+
if (scopedManager.id === managerId) {
330+
this._projectPackageManagers.delete(key);
331+
}
332+
}
333+
this.disposePackageManagerEventSubscriptions(managerId);
331334
setImmediate(() => this._onDidChangePackageManager.fire({ kind: 'unregistered', manager: mgr }));
332335
});
333336
}
334337

335338
public dispose() {
336339
this._environmentManagers.clear();
337340
this._packageManagers.clear();
341+
this._projectPackageManagers.clear();
342+
for (const managerId of this._packageManagerEventSubscriptions.keys()) {
343+
this.disposePackageManagerEventSubscriptions(managerId);
344+
}
338345
this._inlineRoutingOverrides.clear();
339346
this.subscriptions.forEach((subscription) => subscription.dispose());
340347
this._onDidChangeEnvironmentManager.dispose();
@@ -343,6 +350,7 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
343350
this._onDidChangeManagerEnvironment.dispose();
344351
this._onDidChangeActiveEnvironment.dispose();
345352
this._onDidChangePackages.dispose();
353+
this._onDidChangePackageProviderPackages.dispose();
346354
}
347355

348356
/**
@@ -407,17 +415,21 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
407415
}
408416

409417
if (context === undefined || context instanceof Uri) {
418+
const project = context ? this.pm.get(context) : undefined;
410419
const defaultPkgManagerId = getDefaultPkgManagerSetting(this.pm, context);
411420
const defaultEnvManagerId = getDefaultEnvManagerSetting(this.pm, context);
412421
if (defaultPkgManagerId) {
413-
return this._packageManagers.get(defaultPkgManagerId);
422+
return this.getProjectPackageManager(this._packageManagers.get(defaultPkgManagerId), project);
414423
}
415424

416425
if (defaultEnvManagerId) {
417426
const preferredPkgManagerId =
418427
this._environmentManagers.get(defaultEnvManagerId)?.preferredPackageManagerId;
419428
if (preferredPkgManagerId) {
420-
return this._packageManagers.get(preferredPkgManagerId);
429+
return this.getProjectPackageManager(
430+
this._packageManagers.get(preferredPkgManagerId),
431+
project,
432+
);
421433
}
422434
}
423435
return undefined;
@@ -439,6 +451,69 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
439451
return undefined;
440452
}
441453

454+
private getProjectPackageManager(
455+
manager: InternalPackageManager | undefined,
456+
project: PythonProject | undefined,
457+
): InternalPackageManager | undefined {
458+
if (!manager || !project) {
459+
return manager;
460+
}
461+
462+
const key = `${manager.id}:${normalizePath(project.uri.fsPath)}`;
463+
let scopedManager = this._projectPackageManagers.get(key);
464+
if (!scopedManager) {
465+
scopedManager = manager.createProjectScopedManager(project);
466+
if (scopedManager) {
467+
this._projectPackageManagers.set(key, scopedManager);
468+
this.subscribeToPackageManagerEvents(manager, scopedManager);
469+
}
470+
}
471+
return scopedManager ?? manager;
472+
}
473+
474+
private subscribeToPackageManagerEvents(
475+
provider: InternalPackageManager,
476+
manager: InternalPackageManager,
477+
): void {
478+
const event = manager.packageChangeEvent;
479+
if (!event) {
480+
return;
481+
}
482+
483+
let subscriptions = this._packageManagerEventSubscriptions.get(provider.id);
484+
if (!subscriptions) {
485+
subscriptions = new Map();
486+
this._packageManagerEventSubscriptions.set(provider.id, subscriptions);
487+
}
488+
if (subscriptions.has(event)) {
489+
return;
490+
}
491+
492+
subscriptions.set(
493+
event,
494+
event((e) => {
495+
this._onDidChangePackageProviderPackages.fire(e);
496+
const eventManager =
497+
Array.from(this._projectPackageManagers.values()).find(
498+
(candidate) => candidate.id === provider.id && candidate.wraps(e.manager),
499+
) ?? provider;
500+
setImmediate(() =>
501+
this._onDidChangePackages.fire({
502+
environment: e.environment,
503+
manager: eventManager,
504+
changes: e.changes,
505+
}),
506+
);
507+
}),
508+
);
509+
}
510+
511+
private disposePackageManagerEventSubscriptions(managerId: string): void {
512+
const subscriptions = this._packageManagerEventSubscriptions.get(managerId);
513+
subscriptions?.forEach((subscription) => subscription.dispose());
514+
this._packageManagerEventSubscriptions.delete(managerId);
515+
}
516+
442517
public get managers(): InternalEnvironmentManager[] {
443518
return Array.from(this._environmentManagers.values());
444519
}

‎src/managers/common/packageWatcher.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ export function registerPackageWatchers(
153153
return;
154154
}
155155

156-
const watcherKey = `${environment.envId.managerId}:${environment.envId.id}:${selectedPackageManager.id}`;
156+
const packageManagerKey =
157+
`${selectedPackageManager.id}:${selectedPackageManager.project?.uri.toString() ?? ''}`;
158+
const watcherKey = `${environment.envId.managerId}:${environment.envId.id}:${packageManagerKey}`;
157159
if (activeWatcherByConsumer.get(consumer) === watcherKey) {
158160
return;
159161
}

‎src/managers/common/registeredManagers.ts‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT License.
33

44
import type { Pep440Version } from '@renovatebot/pep440';
5-
import { CancellationError, Disposable, LogOutputChannel, MarkdownString, RelativePattern } from 'vscode';
5+
import { CancellationError, Disposable, Event, LogOutputChannel, MarkdownString, RelativePattern } from 'vscode';
66
import { PackageVersionLookupNotSupportedError } from '../../publicErrors';
77
import { ISSUES_URL } from '../../common/constants';
88
import { CreateEnvironmentNotSupported, RemoveEnvironmentNotSupported } from '../../common/errors/NotSupportedError';
@@ -27,6 +27,7 @@ import type {
2727
PackageManagementOptions,
2828
PackageManager,
2929
PythonEnvironment,
30+
PythonProject,
3031
QuickCreateConfig,
3132
RefreshEnvironmentsScope,
3233
RemoveEnvironmentOptions,
@@ -210,10 +211,17 @@ function inferPackageManagementTrigger(
210211
}
211212

212213
export class InternalPackageManager implements PackageManager {
214+
private readonly relatedManagers: WeakSet<PackageManager>;
215+
213216
public constructor(
214217
public readonly id: string,
215218
private readonly manager: PackageManager,
216-
) {}
219+
public readonly project?: PythonProject,
220+
relatedManagers?: WeakSet<PackageManager>,
221+
) {
222+
this.relatedManagers = relatedManagers ?? new WeakSet<PackageManager>();
223+
this.relatedManagers.add(manager);
224+
}
217225

218226
public get name(): string {
219227
return this.manager.name;
@@ -279,10 +287,23 @@ export class InternalPackageManager implements PackageManager {
279287
return this.manager.onDidChangePackages ? this.manager.onDidChangePackages(handler) : new Disposable(() => {});
280288
}
281289

282-
equals(other: PackageManager): boolean {
290+
get packageChangeEvent(): Event<DidChangePackagesEventArgs> | undefined {
291+
return this.manager.onDidChangePackages;
292+
}
293+
294+
wraps(other: PackageManager): boolean {
283295
return this.manager === other;
284296
}
285297

298+
equals(other: PackageManager): boolean {
299+
return this.relatedManagers.has(other);
300+
}
301+
302+
createProjectScopedManager(project: PythonProject): InternalPackageManager | undefined {
303+
const manager = this.manager.createForProject?.(project);
304+
return manager ? new InternalPackageManager(this.id, manager, project, this.relatedManagers) : undefined;
305+
}
306+
286307
getVersion(environment: PythonEnvironment): Promise<Pep440Version | undefined> {
287308
return this.manager.getVersion ? this.manager.getVersion(environment) : Promise.resolve(undefined);
288309
}

0 commit comments

Comments
 (0)