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
1 change: 1 addition & 0 deletions .github/instructions/testing-workflow.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This guide covers the full testing lifecycle:

## Learnings

- Public API manager fixtures must return internal manager wrappers. In execution-policy tests, stub the wrapper's `getVersion()` to isolate operation subprocesses from capability telemetry (1).
- Pip commands that return JSON must pass `--disable-pip-version-check`; the process helper combines stderr with stdout, so update notices can otherwise make valid JSON unparseable (1).
- When a view subscribes to a newly added provider event, TypeMoq-based view tests must return a real `EventEmitter.event`; an unstubbed event yields an undefined disposable and fails during teardown (1).
- Test agent selections across a full reload with `python.defaultInterpreterPath` set. An effective manager value equal to the extension default does not prove a workspace value was saved; tool-owned persistence must inspect `workspaceValue` or startup can restore the global interpreter (1).
Expand Down
9 changes: 9 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1871,6 +1871,15 @@ classification, not exact installation intent. Selection is scoped environment
selection, not shell activation.
Pip reports installation, uninstallation, and upgrades as unsupported for Python 2 environments.

Public environment and package operations initiate the corresponding capability
queries before delegating to managers. These queries are observational: operations
do not wait for them or use their results to gate execution, and probe failures
are logged without changing operation results. They use the existing
`getEnvironmentManagerCapability` and `getPackageManagerCapability` methods,
including their legacy fallback and query telemetry. Package queries include the
routed manager's project when applicable. Registration, item factories, project
APIs, and execution APIs have no corresponding capability checks.

Use the exported `EnvironmentCapability` and `PackageCapability` constants or
their literal string values.
`PackageCapability.ListDirect` is `packages.list.direct`;
Expand Down
78 changes: 75 additions & 3 deletions src/extensionApi.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Disposable, Event, EventEmitter, l10n, TaskExecution, Terminal, Uri } from 'vscode';
import * as path from 'path';
import type { CapabilityContext, EnvironmentManagerCapability, PackageManagerCapability, Support } from './capabilities';
import { isKnownManagerCapability } from './capabilities';
import { EnvironmentCapability, isKnownManagerCapability, PackageCapability } from './capabilities';
import { explain as parsePep440Version } from '@renovatebot/pep440';
import { EventNames } from './common/telemetry/constants';
import { sendTelemetryEvent } from './common/telemetry/sender';
Expand Down Expand Up @@ -84,6 +84,10 @@ type ManagerCapabilityQuery =
context: CapabilityContext & { environment: PythonEnvironment };
};

function logCapabilityQueryError(error: unknown): void {
traceError('Failed to query manager capability:', error);
}

export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
readonly __pythonTools: PythonToolsApi;
private readonly _onDidChangeEnvironments = new EventEmitter<DidChangeEnvironmentsEventArgs>();
Expand Down Expand Up @@ -314,6 +318,8 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
if (!manager) {
throw new Error('No environment manager found');
}
void this.getEnvironmentManagerCapability(manager.id, EnvironmentCapability.Create, { scope })
.catch(logCapabilityQueryError);
if (!manager.supportsCreate) {
throw new Error(`Environment manager does not support creating environments: ${manager.id}`);
}
Expand Down Expand Up @@ -344,6 +350,8 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
throw new Error('No environment manager found');
}

void this.getEnvironmentManagerCapability(manager.id, EnvironmentCapability.Create, { scope })
.catch(logCapabilityQueryError);
const result = await manager.create(scope, options);
return result;
}
Expand All @@ -354,14 +362,20 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
if (!manager) {
return Promise.reject(new Error('No environment manager found'));
}
void this.getEnvironmentManagerCapability(manager.id, EnvironmentCapability.Remove, { environment })
.catch(logCapabilityQueryError);
return manager.remove(environment, options);
}
async refreshEnvironments(scope: RefreshEnvironmentsScope): Promise<void> {
const currentScope = checkUri(scope) as RefreshEnvironmentsScope;

if (currentScope === undefined) {
await waitForAllEnvManagers();
await Promise.all(this.envManagers.managers.map((manager) => manager.refresh(currentScope)));
await Promise.all(this.envManagers.managers.map((manager) => {
void this.getEnvironmentManagerCapability(manager.id, EnvironmentCapability.List, { scope: currentScope })
.catch(logCapabilityQueryError);
return manager.refresh(currentScope);
}));
return Promise.resolve();
}

Expand All @@ -370,13 +384,19 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
if (!manager) {
return Promise.reject(new Error(`No environment manager found for: ${currentScope.fsPath}`));
}
void this.getEnvironmentManagerCapability(manager.id, EnvironmentCapability.List, { scope: currentScope })
.catch(logCapabilityQueryError);
return manager.refresh(currentScope);
}
async getEnvironments(scope: GetEnvironmentsScope): Promise<PythonEnvironment[]> {
const currentScope = checkUri(scope) as GetEnvironmentsScope;
if (currentScope === 'all' || currentScope === 'global') {
await waitForAllEnvManagers();
const promises = this.envManagers.managers.map((manager) => manager.getEnvironments(currentScope));
const promises = this.envManagers.managers.map((manager) => {
void this.getEnvironmentManagerCapability(manager.id, EnvironmentCapability.List, { scope: currentScope })
.catch(logCapabilityQueryError);
return manager.getEnvironments(currentScope);
});
const items = await Promise.all(promises);
return items.flat();
}
Expand All @@ -387,6 +407,8 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
return [];
}

void this.getEnvironmentManagerCapability(manager.id, EnvironmentCapability.List, { scope: currentScope })
.catch(logCapabilityQueryError);
const items = await manager.getEnvironments(currentScope);
return items;
}
Expand All @@ -396,6 +418,29 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
await waitForEnvManager(
currentScope ? (currentScope instanceof Uri ? [currentScope] : currentScope) : undefined,
);
if (Array.isArray(currentScope) && !environment) {
const scopesByManager = new Map<InternalEnvironmentManager, Uri[]>();
for (const uri of currentScope) {
const manager = this.envManagers.getEnvironmentManager(uri);
if (manager) {
scopesByManager.set(manager, [...(scopesByManager.get(manager) ?? []), uri]);
}
}
for (const [manager, scopes] of scopesByManager) {
void this.getEnvironmentManagerCapability(manager.id, EnvironmentCapability.SetSelected, { scope: scopes })
.catch(logCapabilityQueryError);
}
} else {
const manager = this.envManagers.getEnvironmentManager(
environment ?? (currentScope instanceof Uri ? currentScope : undefined),
);
if (manager) {
void this.getEnvironmentManagerCapability(manager.id, EnvironmentCapability.SetSelected, {
scope: currentScope,
environment,
}).catch(logCapabilityQueryError);
}
}
return this.envManagers.setEnvironment(currentScope, environment);
}
async getEnvironment(scope: GetEnvironmentScope): Promise<PythonEnvironment | undefined> {
Expand All @@ -408,6 +453,11 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
// via onDidChangeEnvironment once it settles.
const resolution = (async () => {
await waitForEnvManager(currentScope ? [currentScope] : undefined);
const manager = this.envManagers.getEnvironmentManager(currentScope);
if (manager) {
void this.getEnvironmentManagerCapability(manager.id, EnvironmentCapability.GetSelected, { scope: currentScope })
.catch(logCapabilityQueryError);
}
return this.envManagers.getEnvironment(currentScope);
})();

Expand Down Expand Up @@ -443,6 +493,10 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
}
});

for (const manager of new Set([...projectEnvManagers, ...this.envManagers.managers])) {
void this.getEnvironmentManagerCapability(manager.id, EnvironmentCapability.Resolve)
.catch(logCapabilityQueryError);
}
return await handlePythonPath(context, this.envManagers.managers, projectEnvManagers);
}

Expand All @@ -455,6 +509,18 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
if (!manager) {
return Promise.reject(new Error('No package manager found'));
}
const hasInstall = !!options.install?.length;
const hasUninstall = !!options.uninstall?.length;
const interactive = !hasInstall && !hasUninstall;
for (const [capability, requested] of [
[options.upgrade ? PackageCapability.Upgrade : PackageCapability.Install, hasInstall || interactive],
[PackageCapability.Uninstall, hasUninstall || interactive],
[PackageCapability.Install, interactive && options.upgrade],
] as const) {
if (requested) {
void this.getPackageManagerCapability(context, capability, manager.project).catch(logCapabilityQueryError);
}
}
return manager.manage(context, options);
}
async refreshPackages(context: PythonEnvironment): Promise<void> {
Expand All @@ -463,6 +529,8 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
if (!manager) {
return Promise.reject(new Error('No package manager found'));
}
void this.getPackageManagerCapability(context, PackageCapability.List, manager.project)
.catch(logCapabilityQueryError);
return manager.refresh(context);
}
async getPackages(context: PythonEnvironment, options?: GetPackagesOptions): Promise<Package[] | undefined> {
Expand All @@ -471,6 +539,8 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
if (!manager) {
return Promise.resolve(undefined);
}
void this.getPackageManagerCapability(context, PackageCapability.List, manager.project)
.catch(logCapabilityQueryError);
Comment on lines +542 to +543
return manager.getPackages(context, options);
}
getPackageAvailableVersions(
Expand Down Expand Up @@ -498,6 +568,8 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
}
return undefined;
}
void this.getPackageManagerCapability(context, PackageCapability.FetchAvailableVersions, manager.project)
.catch(logCapabilityQueryError);
return manager.getPackageAvailableVersions(context, packageName, options);
}
onDidChangePackages: Event<DidChangePackagesEventArgs> = this._onDidChangePackages.event;
Expand Down
Loading
Loading