Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
FileChangeType,
LogOutputChannel,
MarkdownString,
RelativePattern,
TaskExecution,
Terminal,
TerminalOptions,
Expand Down Expand Up @@ -687,6 +688,16 @@ export interface PackageManager {
*/
getPackages(environment: PythonEnvironment, options?: GetPackagesOptions): Promise<Package[] | undefined>;

/**
* Returns additional filesystem patterns to watch for package install/uninstall changes.
*
* These patterns are appended to the default site-packages metadata locations.
* Implement this for manager-specific locations (for example, conda-meta).
*
* @param environment - The Python environment whose package paths should be watched.
* @returns Relative patterns to watch for package changes.
*/
getPackageWatchTargets?(environment: PythonEnvironment): RelativePattern[];
/**
* Event that is fired when packages change.
*/
Expand Down
12 changes: 12 additions & 0 deletions examples/sample1/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
FileChangeType,
LogOutputChannel,
MarkdownString,
RelativePattern,
TaskExecution,
Terminal,
TerminalOptions,
Expand Down Expand Up @@ -616,6 +617,17 @@ export interface PackageManager {
*/
getPackages(environment: PythonEnvironment, options?: GetPackagesOptions): Promise<Package[] | undefined>;

/**
* Returns additional filesystem patterns to watch for package install/uninstall changes.
*
* These patterns are appended to the default site-packages metadata locations.
* Implement this for manager-specific locations (for example, conda-meta).
*
* @param environment - The Python environment whose package paths should be watched.
* @returns Relative patterns to watch for package changes.
*/
getPackageWatchTargets?(environment: PythonEnvironment): RelativePattern[];

/**
* Event that is fired when packages change.
*/
Expand Down
12 changes: 12 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
FileChangeType,
LogOutputChannel,
MarkdownString,
RelativePattern,
TaskExecution,
Terminal,
TerminalOptions,
Expand Down Expand Up @@ -681,6 +682,17 @@ export interface PackageManager {
*/
getPackages(environment: PythonEnvironment, options?: GetPackagesOptions): Promise<Package[] | undefined>;

/**
* Returns additional filesystem patterns to watch for package install/uninstall changes.
*
* These patterns are appended to the default site-packages metadata locations.
* Implement this for manager-specific locations (for example, conda-meta).
*
* @param environment - The Python environment whose package paths should be watched.
* @returns Relative patterns to watch for package changes.
*/
getPackageWatchTargets?(environment: PythonEnvironment): RelativePattern[];

/**
* Event that is fired when packages change.
*/
Expand Down
35 changes: 3 additions & 32 deletions src/managers/builtin/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createSimpleDebounce } from '../../common/utils/debounce';
import { createFileSystemWatcher, onDidDeleteFiles } from '../../common/workspace.apis';
import { getPythonApi } from '../../features/pythonApi';
import { NativePythonFinder } from '../common/nativePythonFinder';
import { registerPackageWatcherForManager } from '../common/packageWatcher';
import { PipPackageManager } from './pipPackageManager';
import { SysPythonManager } from './sysPythonManager';
import { VenvManager } from './venvManager';
Expand Down Expand Up @@ -41,38 +42,8 @@ export async function registerSystemPythonFeatures(
}),
);

const packageDebouncedRefresh = createSimpleDebounce(500, async () => {
const projects = await api.getPythonProjects();
await Promise.all(
projects.map(async (project) => {
const env = await api.getEnvironment(project.uri);
if (!env) {
return;
}
try {
await api.refreshPackages(env);
} catch (ex) {
log.error(
`Failed to refresh packages for environment ${env.envId}: ${ex instanceof Error ? ex.message : String(ex)}`,
);
}
}),
);
});
const packageWatcher = createFileSystemWatcher(
'**/site-packages/*.dist-info/METADATA',
false, // don't ignore create events (pip install)
true, // ignore change events (content changes in METADATA don't affect package list)
false, // don't ignore delete events (pip uninstall)
);
disposables.push(
packageDebouncedRefresh,
packageWatcher,
packageWatcher.onDidCreate(() => {
packageDebouncedRefresh.trigger();
}),
packageWatcher.onDidDelete(() => {
packageDebouncedRefresh.trigger();
}),
await registerPackageWatcherForManager(envManager, pkgManager, log),
await registerPackageWatcherForManager(venvManager, pkgManager, log),
);
}
125 changes: 125 additions & 0 deletions src/managers/common/packageWatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import * as path from 'path';
import { Disposable, LogOutputChannel, RelativePattern } from 'vscode';
import { EnvironmentManager, PackageManager, PythonEnvironment } from '../../api';
import { traceVerbose } from '../../common/logging';
import { createSimpleDebounce } from '../../common/utils/debounce';
import { createFileSystemWatcher } from '../../common/workspace.apis';

/**
* Derives the file system watch targets for a given Python environment.
*
* Targets include site-packages `.dist-info/METADATA` files for pip-style installs.
*
* @param env - The Python environment to derive watch targets for.
* @returns An array of RelativePattern objects, one per discoverable package location.
* Empty if the environment has no `sysPrefix` or discoverable paths.
*/
function getDefaultPackageWatchTargets(env: PythonEnvironment): RelativePattern[] {
if (!env.sysPrefix) {
return [];
}
return process.platform === 'win32'
? [new RelativePattern(path.join(env.sysPrefix, 'Lib'), 'site-packages/**/*.dist-info/METADATA')] // Windows
: [new RelativePattern(path.join(env.sysPrefix, 'lib'), 'python*/site-packages/**/*.dist-info/METADATA')]; // Unix-like
}

/**
* Creates a file system watcher for package changes in a single environment.
*
* Monitors default site-packages locations and any manager-specific extra locations
* for install/uninstall operations.
* and triggers a debounced package refresh when changes are detected.
*
* @param env - The Python environment to watch.
* @param packageManager - The package manager to call refresh on when changes occur.
* @param log - Logger for diagnostic messages.
* @returns A disposable that removes the watcher when disposed.
*/
export function watchPackageChangesForEnvironment(
env: PythonEnvironment,
packageManager: PackageManager,
log: LogOutputChannel,
): Disposable {
// Watch targets
const watchTargets = [
...getDefaultPackageWatchTargets(env),
...(packageManager.getPackageWatchTargets?.(env) ?? []),
];
if (watchTargets.length === 0) {
traceVerbose(log, `No watch targets for environment ${env.envId}`);
return new Disposable(() => undefined);
}
// Debounced refresh function
const debouncedRefresh = createSimpleDebounce(500, async () => {
traceVerbose(log, `Package change detected for environment ${env.envId.id}, refreshing packages.`);
packageManager.refresh(env).catch((ex) => {
log.error(
`Failed to refresh packages for environment ${env.envId}: ${ex instanceof Error ? ex.message : String(ex)}`,
);
});
});
// Create watchers
const disposables: Disposable[] = [];
for (const target of watchTargets) {
const watcher = createFileSystemWatcher(
target,
false, // create -> install
false, // change -> ignore
false, // delete -> uninstall
);
disposables.push(
watcher,
watcher.onDidCreate(debouncedRefresh.trigger),
watcher.onDidDelete(debouncedRefresh.trigger),
);
}

return new Disposable(() => disposables.forEach((d) => d.dispose()));
}

/**
* Registers automatic file system watchers for the active environment managed by a given manager.
*
* Creates per-environment watchers that are attached when the active environment changes
* and detached when it changes to a different environment. Ensures package changes
* (installs/uninstalls) in the active environment are detected and trigger a refresh.
*
* @param envManager - The environment manager whose active environment should be watched.
* @param packageManager - The package manager to call refresh on when changes occur.
* @param log - Logger for diagnostic and error messages.
* @returns A disposable that removes all watchers and subscriptions when disposed.
*/
export async function registerPackageWatcherForManager(
envManager: EnvironmentManager,
packageManager: PackageManager,
log: LogOutputChannel,
): Promise<Disposable> {
// One watcher per environment id.
const watchers = new Map<string, Disposable>();

const addWatcher = (env: PythonEnvironment): void => {
if (!watchers.has(env.envId.id)) {
watchers.set(env.envId.id, watchPackageChangesForEnvironment(env, packageManager, log));
}
};

const removeWatcher = (envId: string): void => {
watchers.get(envId)?.dispose();
watchers.delete(envId);
};

const envChangeDisposable = envManager.onDidChangeEnvironment?.((changes) => {
if (changes.new) {
addWatcher(changes.new);
}
if (changes.old) {
removeWatcher(changes.old.envId.id);
}
});

return new Disposable(() => {
envChangeDisposable?.dispose();
watchers.forEach((watcher) => watcher.dispose());
watchers.clear();
});
}
10 changes: 10 additions & 0 deletions src/managers/conda/condaPackageManager.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as path from 'path';
import {
CancellationError,
Disposable,
Expand All @@ -6,6 +7,7 @@ import {
LogOutputChannel,
MarkdownString,
ProgressLocation,
RelativePattern,
} from 'vscode';
import {
DidChangePackagesEventArgs,
Expand Down Expand Up @@ -150,6 +152,14 @@ export class CondaPackageManager implements PackageManager, Disposable {
return this.packages.get(environment.envId.id);
}

getPackageWatchTargets(environment: PythonEnvironment): RelativePattern[] {
if (!environment.sysPrefix) {
return [];
}

return [new RelativePattern(path.join(environment.sysPrefix, 'conda-meta'), '**/*.json')];
}

dispose() {
this._onDidChangePackages.dispose();
this.packages.clear();
Expand Down
2 changes: 2 additions & 0 deletions src/managers/conda/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { sendTelemetryEvent } from '../../common/telemetry/sender';
import { getPythonApi } from '../../features/pythonApi';
import { PythonProjectManager } from '../../internal.api';
import { NativePythonFinder } from '../common/nativePythonFinder';
import { registerPackageWatcherForManager } from '../common/packageWatcher';
import { notifyMissingManagerIfDefault } from '../common/utils';
import { CondaEnvManager } from './condaEnvManager';
import { CondaPackageManager } from './condaPackageManager';
Expand Down Expand Up @@ -55,6 +56,7 @@ export async function registerCondaFeatures(
packageManager,
api.registerEnvironmentManager(envManager),
api.registerPackageManager(packageManager),
await registerPackageWatcherForManager(envManager, packageManager, log),
);
} catch (ex) {
await notifyMissingManagerIfDefault('ms-python.python:conda', projectManager, api);
Expand Down
2 changes: 2 additions & 0 deletions src/managers/poetry/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { traceInfo } from '../../common/logging';
import { getPythonApi } from '../../features/pythonApi';
import { PythonProjectManager } from '../../internal.api';
import { NativePythonFinder } from '../common/nativePythonFinder';
import { registerPackageWatcherForManager } from '../common/packageWatcher';
import { PoetryManager } from './poetryManager';
import { PoetryPackageManager } from './poetryPackageManager';

Expand All @@ -24,5 +25,6 @@ export async function registerPoetryFeatures(
pkgManager,
api.registerEnvironmentManager(envManager),
api.registerPackageManager(pkgManager),
await registerPackageWatcherForManager(envManager, pkgManager, outputChannel),
);
}
Loading
Loading