Skip to content

Commit feb0677

Browse files
committed
Add site-packages watchers
1 parent db454e6 commit feb0677

4 files changed

Lines changed: 120 additions & 32 deletions

File tree

src/managers/builtin/main.ts

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createSimpleDebounce } from '../../common/utils/debounce';
44
import { createFileSystemWatcher, onDidDeleteFiles } from '../../common/workspace.apis';
55
import { getPythonApi } from '../../features/pythonApi';
66
import { NativePythonFinder } from '../common/nativePythonFinder';
7+
import { registerPackageWatcherForManager } from '../common/packageWatcher';
78
import { PipPackageManager } from './pipPackageManager';
89
import { SysPythonManager } from './sysPythonManager';
910
import { VenvManager } from './venvManager';
@@ -41,38 +42,8 @@ export async function registerSystemPythonFeatures(
4142
}),
4243
);
4344

44-
const packageDebouncedRefresh = createSimpleDebounce(500, async () => {
45-
const projects = await api.getPythonProjects();
46-
await Promise.all(
47-
projects.map(async (project) => {
48-
const env = await api.getEnvironment(project.uri);
49-
if (!env) {
50-
return;
51-
}
52-
try {
53-
await api.refreshPackages(env);
54-
} catch (ex) {
55-
log.error(
56-
`Failed to refresh packages for environment ${env.envId}: ${ex instanceof Error ? ex.message : String(ex)}`,
57-
);
58-
}
59-
}),
60-
);
61-
});
62-
const packageWatcher = createFileSystemWatcher(
63-
'**/site-packages/*.dist-info/METADATA',
64-
false, // don't ignore create events (pip install)
65-
true, // ignore change events (content changes in METADATA don't affect package list)
66-
false, // don't ignore delete events (pip uninstall)
67-
);
6845
disposables.push(
69-
packageDebouncedRefresh,
70-
packageWatcher,
71-
packageWatcher.onDidCreate(() => {
72-
packageDebouncedRefresh.trigger();
73-
}),
74-
packageWatcher.onDidDelete(() => {
75-
packageDebouncedRefresh.trigger();
76-
}),
46+
await registerPackageWatcherForManager(envManager, pkgManager, log),
47+
await registerPackageWatcherForManager(venvManager, pkgManager, log),
7748
);
7849
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import * as path from 'path';
2+
import { Disposable, LogOutputChannel, RelativePattern, Uri } from 'vscode';
3+
import { EnvironmentChangeKind, EnvironmentManager, PackageManager, PythonEnvironment } from '../../api';
4+
import { traceVerbose } from '../../common/logging';
5+
import { createSimpleDebounce } from '../../common/utils/debounce';
6+
import { createFileSystemWatcher } from '../../common/workspace.apis';
7+
8+
function getWatchTargets(env: PythonEnvironment): RelativePattern[] {
9+
if (!env.sysPrefix) return [];
10+
const targets: RelativePattern[] = [];
11+
12+
if (process.platform === 'win32') {
13+
targets.push(
14+
new RelativePattern(Uri.file(path.join(env.sysPrefix, 'Lib')), 'site-packages/**/*.dist-info/METADATA'),
15+
);
16+
} else {
17+
targets.push(
18+
new RelativePattern(
19+
Uri.file(path.join(env.sysPrefix, 'lib')),
20+
'python*/site-packages/**/*.dist-info/METADATA',
21+
),
22+
);
23+
}
24+
25+
// Conda
26+
targets.push(new RelativePattern(Uri.file(path.join(env.sysPrefix, 'conda-meta')), '**/*.json'));
27+
28+
return targets;
29+
}
30+
31+
export function watchPackageChangesForEnvironment(
32+
env: PythonEnvironment,
33+
packageManager: PackageManager,
34+
log: LogOutputChannel,
35+
): Disposable {
36+
// Watch targets
37+
const watchTargets = getWatchTargets(env);
38+
if (watchTargets.length === 0) {
39+
traceVerbose(log, `No watch targets for environment ${env.envId}`);
40+
return new Disposable(() => undefined);
41+
}
42+
// Debounced refresh function
43+
const debouncedRefresh = createSimpleDebounce(500, async () => {
44+
packageManager.refresh(env).catch((ex) => {
45+
log.error(
46+
`Failed to refresh packages for environment ${env.envId}: ${ex instanceof Error ? ex.message : String(ex)}`,
47+
);
48+
});
49+
});
50+
// Create watchers
51+
const disposables: Disposable[] = [];
52+
for (const target of watchTargets) {
53+
const watcher = createFileSystemWatcher(
54+
target,
55+
true, // create -> install
56+
false, // change -> ignore
57+
true, // delete -> uninstall
58+
);
59+
disposables.push(
60+
watcher,
61+
watcher.onDidCreate(debouncedRefresh.trigger),
62+
watcher.onDidDelete(debouncedRefresh.trigger),
63+
);
64+
}
65+
66+
return new Disposable(() => disposables.forEach((d) => d.dispose()));
67+
}
68+
69+
/**
70+
* Registers package file watchers for all environments managed by the given manager.
71+
*
72+
* This is project-agnostic: if a manager discovers an environment, we watch it.
73+
*/
74+
export async function registerPackageWatcherForManager(
75+
envManager: EnvironmentManager,
76+
packageManager: PackageManager,
77+
log: LogOutputChannel,
78+
): Promise<Disposable> {
79+
// One watcher per environment id.
80+
const watchers = new Map<string, Disposable>();
81+
82+
const addWatcher = (env: PythonEnvironment): void => {
83+
if (!watchers.has(env.envId.id)) {
84+
watchers.set(env.envId.id, watchPackageChangesForEnvironment(env, packageManager, log));
85+
}
86+
};
87+
88+
const removeWatcher = (envId: string): void => {
89+
watchers.get(envId)?.dispose();
90+
watchers.delete(envId);
91+
};
92+
93+
// Keep watchers in sync with environment discovery/removal events.
94+
const envChangeDisposable = envManager.onDidChangeEnvironments?.((changes) => {
95+
changes.forEach((change) => {
96+
if (change.kind === EnvironmentChangeKind.add) {
97+
addWatcher(change.environment);
98+
} else {
99+
removeWatcher(change.environment.envId.id);
100+
}
101+
});
102+
});
103+
104+
// Seed with environments that already exist before this subscription.
105+
const environments = await envManager.getEnvironments('all');
106+
environments.forEach(addWatcher);
107+
108+
return new Disposable(() => {
109+
envChangeDisposable?.dispose();
110+
watchers.forEach((watcher) => watcher.dispose());
111+
watchers.clear();
112+
});
113+
}

src/managers/conda/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { sendTelemetryEvent } from '../../common/telemetry/sender';
66
import { getPythonApi } from '../../features/pythonApi';
77
import { PythonProjectManager } from '../../internal.api';
88
import { NativePythonFinder } from '../common/nativePythonFinder';
9+
import { registerPackageWatcherForManager } from '../common/packageWatcher';
910
import { notifyMissingManagerIfDefault } from '../common/utils';
1011
import { CondaEnvManager } from './condaEnvManager';
1112
import { CondaPackageManager } from './condaPackageManager';
@@ -55,6 +56,7 @@ export async function registerCondaFeatures(
5556
packageManager,
5657
api.registerEnvironmentManager(envManager),
5758
api.registerPackageManager(packageManager),
59+
await registerPackageWatcherForManager(envManager, packageManager, log),
5860
);
5961
} catch (ex) {
6062
await notifyMissingManagerIfDefault('ms-python.python:conda', projectManager, api);

src/managers/poetry/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { traceInfo } from '../../common/logging';
44
import { getPythonApi } from '../../features/pythonApi';
55
import { PythonProjectManager } from '../../internal.api';
66
import { NativePythonFinder } from '../common/nativePythonFinder';
7+
import { registerPackageWatcherForManager } from '../common/packageWatcher';
78
import { PoetryManager } from './poetryManager';
89
import { PoetryPackageManager } from './poetryPackageManager';
910

@@ -24,5 +25,6 @@ export async function registerPoetryFeatures(
2425
pkgManager,
2526
api.registerEnvironmentManager(envManager),
2627
api.registerPackageManager(pkgManager),
28+
await registerPackageWatcherForManager(envManager, pkgManager, outputChannel),
2729
);
2830
}

0 commit comments

Comments
 (0)