Skip to content

Commit f1f409f

Browse files
committed
Shared cache
1 parent d4dbfb6 commit f1f409f

5 files changed

Lines changed: 291 additions & 38 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ manager explicitly always uses uv, independently of that setting. The uv package
9595
uv from PATH, a current-session installation, or the requesting trusted workspace's `.pyprojectx/main/uv`
9696
(`uv.exe` on Windows). If uv is unavailable, it reports an error rather than falling back to pip.
9797
Scoped requests use the project to discover uv even when the target environment is outside the workspace.
98+
Package mutations invalidate both built-in backends' cached inventory for the target environment,
99+
so switching between pip and uv does not reuse a snapshot from before the mutation.
98100

99101
#### Default Package Manager by Environment Manager
100102

‎src/managers/builtin/builtInPackageManager.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@ import { UvPackageManager } from './uvPackageManager';
1818

1919
/** Routes the existing built-in pip entry point to independent pip and uv managers. */
2020
export class BuiltInPackageManager implements PackageManager, Disposable {
21+
private readonly subscriptions: Disposable[];
22+
2123
constructor(
2224
private readonly pip: PipPackageManager,
2325
private readonly uv: UvPackageManager,
2426
) {
2527
this.pip.setPackageManagerOwner(this);
28+
this.subscriptions = [
29+
this.pip.onDidMutatePackages((environment) => this.uv.invalidatePackages(environment)),
30+
this.uv.onDidMutatePackages((environment) => this.pip.invalidatePackages(environment)),
31+
];
2632
}
2733

2834
readonly name = 'pip';
@@ -122,6 +128,7 @@ export class BuiltInPackageManager implements PackageManager, Disposable {
122128

123129
/** Releases the pip fallback owned by this registered entry point. */
124130
dispose(): void {
131+
this.subscriptions.forEach((subscription) => subscription.dispose());
125132
this.pip.dispose();
126133
}
127134
}

‎src/managers/builtin/pipPackageManager.ts‎

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,11 @@ export class PipPackageManager implements PackageManager, Disposable {
8888

8989
private readonly _onDidChangePackages = new EventEmitter<DidChangePackagesEventArgs>();
9090
onDidChangePackages: Event<DidChangePackagesEventArgs> = this._onDidChangePackages.event;
91+
private readonly _onDidMutatePackages = new EventEmitter<PythonEnvironment>();
92+
readonly onDidMutatePackages = this._onDidMutatePackages.event;
9193

9294
private packages: Map<string, Package[]> = new Map();
95+
private readonly packageCacheRevisions = new Map<string, number>();
9396
private packageManagerOwner: PackageManager = this;
9497

9598
constructor(
@@ -108,6 +111,16 @@ export class PipPackageManager implements PackageManager, Disposable {
108111
this.packageManagerOwner = owner;
109112
}
110113

114+
/**
115+
* Evicts cached inventory and prevents older reads from restoring it.
116+
* @param environment Environment whose inventory may have changed.
117+
*/
118+
invalidatePackages(environment: PythonEnvironment): void {
119+
const id = environment.envId.id;
120+
this.packages.delete(id);
121+
this.packageCacheRevisions.set(id, (this.packageCacheRevisions.get(id) ?? 0) + 1);
122+
}
123+
111124
readonly [pythonToolSupport]: PackageToolSupport = {
112125
manage: (environment, options, token, scope) =>
113126
this.manage(environment, { ...options, runHeadless: true }, token, scope),
@@ -162,6 +175,7 @@ export class PipPackageManager implements PackageManager, Disposable {
162175
}
163176

164177
const execute = async (token?: CancellationToken): Promise<void> => {
178+
const before = this.packages.get(environment.envId.id);
165179
try {
166180
if (!environment.execInfo?.run?.executable) {
167181
throw new Error('Unable to determine Python executable path');
@@ -174,29 +188,39 @@ export class PipPackageManager implements PackageManager, Disposable {
174188
toolExecution: !!toolToken,
175189
};
176190

177-
if (toUninstall.length > 0) {
178-
const command = new PipUninstallCommand(manageCommandOptions);
179-
await command.execute({
180-
packages: parsePackageSpecs(toUninstall),
181-
cancellationToken: token,
182-
toolExecution: !!toolToken,
183-
});
184-
}
191+
let mutationStarted = false;
192+
try {
193+
if (toUninstall.length > 0) {
194+
const command = new PipUninstallCommand(manageCommandOptions);
195+
mutationStarted = true;
196+
await command.execute({
197+
packages: parsePackageSpecs(toUninstall),
198+
cancellationToken: token,
199+
toolExecution: !!toolToken,
200+
});
201+
}
185202

186-
if (toInstall.length > 0) {
187-
const command = new PipInstallCommand(manageCommandOptions);
188-
await command.execute({
189-
packages: parsePackageSpecs(toInstall),
190-
upgrade: options.upgrade,
191-
cancellationToken: token,
192-
toolExecution: !!toolToken,
193-
});
203+
if (toInstall.length > 0) {
204+
const command = new PipInstallCommand(manageCommandOptions);
205+
mutationStarted = true;
206+
await command.execute({
207+
packages: parsePackageSpecs(toInstall),
208+
upgrade: options.upgrade,
209+
cancellationToken: token,
210+
toolExecution: !!toolToken,
211+
});
212+
}
213+
} finally {
214+
if (mutationStarted) {
215+
this.invalidatePackages(environment);
216+
this._onDidMutatePackages.fire(environment);
217+
}
194218
}
195219

196220
await updatePackagesAndNotify(
197221
this,
198222
environment,
199-
this.packages.get(environment.envId.id),
223+
before,
200224
(changes) => {
201225
this._onDidChangePackages.fire({ environment, manager: this.packageManagerOwner, changes });
202226
},
@@ -248,6 +272,7 @@ export class PipPackageManager implements PackageManager, Disposable {
248272
title: l10n.t('Refreshing packages'),
249273
},
250274
async () => {
275+
const revision = this.packageCacheRevisions.get(environment.envId.id);
251276
const packages = await updatePackagesAndNotify(
252277
this,
253278
environment,
@@ -256,7 +281,7 @@ export class PipPackageManager implements PackageManager, Disposable {
256281
this._onDidChangePackages.fire({ environment, manager: this.packageManagerOwner, changes });
257282
},
258283
);
259-
if (packages !== undefined) {
284+
if (packages !== undefined && revision === this.packageCacheRevisions.get(environment.envId.id)) {
260285
this.packages.set(environment.envId.id, packages);
261286
}
262287
},
@@ -276,6 +301,7 @@ export class PipPackageManager implements PackageManager, Disposable {
276301
showErrors = true,
277302
token?: CancellationToken,
278303
): Promise<Package[] | undefined> {
304+
const revision = this.packageCacheRevisions.get(environment.envId.id);
279305
try {
280306
throwIfCancelled(token);
281307
if (!environment.execInfo?.run?.executable) {
@@ -292,7 +318,9 @@ export class PipPackageManager implements PackageManager, Disposable {
292318
);
293319
throwIfCancelled(token);
294320
const packages = data.map((pkg) => this.api.createPackageItem(pkg, environment, this.packageManagerOwner));
295-
this.packages.set(environment.envId.id, packages);
321+
if (revision === this.packageCacheRevisions.get(environment.envId.id)) {
322+
this.packages.set(environment.envId.id, packages);
323+
}
296324
return packages;
297325
} catch (error) {
298326
this.log.error('Error refreshing packages', error);
@@ -362,7 +390,9 @@ export class PipPackageManager implements PackageManager, Disposable {
362390
/** Releases package-change listeners and cached packages. */
363391
dispose(): void {
364392
this._onDidChangePackages.dispose();
393+
this._onDidMutatePackages.dispose();
365394
this.packages.clear();
395+
this.packageCacheRevisions.clear();
366396
}
367397

368398
/**

‎src/managers/builtin/uvPackageManager.ts‎

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,27 @@ export class UvPackageManager implements PackageManager, Disposable {
7171

7272
private readonly _onDidChangePackages = new EventEmitter<DidChangePackagesEventArgs>();
7373
readonly onDidChangePackages: Event<DidChangePackagesEventArgs> = this._onDidChangePackages.event;
74+
private readonly _onDidMutatePackages = new EventEmitter<PythonEnvironment>();
75+
readonly onDidMutatePackages = this._onDidMutatePackages.event;
7476
private readonly packages = new Map<string, Package[]>();
77+
private readonly packageCacheRevisions = new Map<string, number>();
7578

7679
constructor(
7780
private readonly api: PythonEnvironmentApi,
7881
public readonly log: LogOutputChannel,
7982
private readonly venv: BuiltInEnvironmentManager,
8083
) {}
8184

85+
/**
86+
* Evicts cached inventory and prevents older reads from restoring it.
87+
* @param environment Environment whose inventory may have changed.
88+
*/
89+
invalidatePackages(environment: PythonEnvironment): void {
90+
const id = environment.envId.id;
91+
this.packages.delete(id);
92+
this.packageCacheRevisions.set(id, (this.packageCacheRevisions.get(id) ?? 0) + 1);
93+
}
94+
8295
readonly [pythonToolSupport]: PackageToolSupport = {
8396
manage: (environment, options, token, scope) =>
8497
this.manage(environment, { ...options, runHeadless: true }, token, scope),
@@ -144,33 +157,44 @@ export class UvPackageManager implements PackageManager, Disposable {
144157
throw new Error(support.reason);
145158
}
146159
const execute = async (token?: CancellationToken): Promise<void> => {
160+
const before = this.packages.get(environment.envId.id);
147161
try {
148162
const commandOptions = {
149163
cwd: scope?.fsPath,
150164
cancellationToken: token,
151165
toolExecution: !!toolToken,
152166
};
153-
if (toUninstall.length > 0) {
154-
const command = new UvUninstallCommand(await this.getCommandOptions(environment, commandOptions));
155-
await command.execute({
156-
packages: parsePackageSpecs(toUninstall),
157-
cancellationToken: token,
158-
toolExecution: !!toolToken,
159-
});
160-
}
161-
if (toInstall.length > 0) {
162-
const command = new UvInstallCommand(await this.getCommandOptions(environment, commandOptions));
163-
await command.execute({
164-
packages: parsePackageSpecs(toInstall),
165-
upgrade: options.upgrade,
166-
cancellationToken: token,
167-
toolExecution: !!toolToken,
168-
});
167+
let mutationStarted = false;
168+
try {
169+
if (toUninstall.length > 0) {
170+
const command = new UvUninstallCommand(await this.getCommandOptions(environment, commandOptions));
171+
mutationStarted = true;
172+
await command.execute({
173+
packages: parsePackageSpecs(toUninstall),
174+
cancellationToken: token,
175+
toolExecution: !!toolToken,
176+
});
177+
}
178+
if (toInstall.length > 0) {
179+
const command = new UvInstallCommand(await this.getCommandOptions(environment, commandOptions));
180+
mutationStarted = true;
181+
await command.execute({
182+
packages: parsePackageSpecs(toInstall),
183+
upgrade: options.upgrade,
184+
cancellationToken: token,
185+
toolExecution: !!toolToken,
186+
});
187+
}
188+
} finally {
189+
if (mutationStarted) {
190+
this.invalidatePackages(environment);
191+
this._onDidMutatePackages.fire(environment);
192+
}
169193
}
170194
await updatePackagesAndNotify(
171195
this,
172196
environment,
173-
this.packages.get(environment.envId.id),
197+
before,
174198
(changes) => this._onDidChangePackages.fire({ environment, manager: this, changes }),
175199
() => this.fetchPackages(environment, !options.runHeadless, toolToken, scope),
176200
!!toolToken,
@@ -212,11 +236,12 @@ export class UvPackageManager implements PackageManager, Disposable {
212236
await withProgress(
213237
{ location: ProgressLocation.Window, title: l10n.t('Refreshing packages') },
214238
async () => {
239+
const revision = this.packageCacheRevisions.get(environment.envId.id);
215240
const packages = await updatePackagesAndNotify(
216241
this, environment, this.packages.get(environment.envId.id),
217242
(changes) => this._onDidChangePackages.fire({ environment, manager: this, changes }),
218243
);
219-
if (packages !== undefined) {
244+
if (packages !== undefined && revision === this.packageCacheRevisions.get(environment.envId.id)) {
220245
this.packages.set(environment.envId.id, packages);
221246
}
222247
},
@@ -237,6 +262,7 @@ export class UvPackageManager implements PackageManager, Disposable {
237262
token?: CancellationToken,
238263
scope?: Uri,
239264
): Promise<Package[] | undefined> {
265+
const revision = this.packageCacheRevisions.get(environment.envId.id);
240266
try {
241267
throwIfCancelled(token);
242268
const command = new UvListCommand(await this.getCommandOptions(environment, {
@@ -247,7 +273,9 @@ export class UvPackageManager implements PackageManager, Disposable {
247273
);
248274
throwIfCancelled(token);
249275
const packages = data.map((pkg) => this.api.createPackageItem(pkg, environment, this));
250-
this.packages.set(environment.envId.id, packages);
276+
if (revision === this.packageCacheRevisions.get(environment.envId.id)) {
277+
this.packages.set(environment.envId.id, packages);
278+
}
251279
return packages;
252280
} catch (error) {
253281
this.log.error('Error refreshing packages', error);
@@ -294,6 +322,8 @@ export class UvPackageManager implements PackageManager, Disposable {
294322
/** Releases package-change listeners and cached packages. */
295323
dispose(): void {
296324
this._onDidChangePackages.dispose();
325+
this._onDidMutatePackages.dispose();
297326
this.packages.clear();
327+
this.packageCacheRevisions.clear();
298328
}
299329
}

0 commit comments

Comments
 (0)