Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4394fb4
API: Advertise EnvironmentManager and PackageManager capabilities
edvilme Oct 2, 2026
4afb37f
Update default values
edvilme Oct 2, 2026
6a35953
Consolidate capabilities and default values into single source of truth
edvilme Oct 2, 2026
70e9e41
Rename to PackageManagerCapability and EnvironmentManagerCapability
edvilme Oct 2, 2026
e32bd70
Defer manager capability runtime preflight validation
edvilme Oct 2, 2026
18baa95
Simplify tests
edvilme Oct 2, 2026
0165f57
Use only registererd managers
edvilme Oct 2, 2026
c813460
Simplify tests
edvilme Oct 2, 2026
ceb1c03
Ground tests
edvilme Oct 2, 2026
f682c63
Address manager capability review feedback
edvilme Oct 2, 2026
accbe46
Reduce public exports
edvilme Oct 2, 2026
03012ed
fix: harden manager capability resolution
edvilme Oct 2, 2026
7f895e9
docs: simplify manager capability guidance
edvilme Oct 2, 2026
b172b7a
Remove default values
edvilme Oct 8, 2026
60f7ce3
Add capability supported helper methods
edvilme Oct 8, 2026
25344dc
Simplify implementation
edvilme Oct 8, 2026
e761cf8
Update test
edvilme Oct 8, 2026
0eee36e
Simplify test
edvilme Oct 8, 2026
ccf3a99
Fix tests that did not exercise their intended behavior
edvilme Oct 8, 2026
ecaf237
Remove capability tests that duplicate existing package-manager routi…
edvilme Oct 8, 2026
7cfa7f0
Remove registry lookup error test that only verified default throw pr…
edvilme Oct 8, 2026
f767d83
Simplify capabilities.ts
edvilme Oct 8, 2026
e5e820e
Update README
edvilme Oct 8, 2026
dd91fd6
Include capabilities in public API change checks
Copilot Oct 8, 2026
570fea8
Add required capabilities to mock managers after rebase onto main
edvilme Oct 9, 2026
54c5f2f
Probe pip/uv version lookup support via getPackageAvailableVersionsCo…
edvilme Oct 9, 2026
052dd9a
Remove environments.create.quick capability key
edvilme Oct 9, 2026
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
3 changes: 3 additions & 0 deletions .github/workflows/pr-file-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
src/api.ts
src/types.ts
src/publicErrors.ts
src/capabilities.ts
file-pattern: 'api/package.json'
skip-label: 'skip api version'
failure-message: 'The public API (${prereq-pattern}) was changed without bumping the package version in ${file-pattern} (the ${skip-label} label can be used to pass this check)'
Expand All @@ -60,6 +61,7 @@ jobs:
src/api.ts
src/types.ts
src/publicErrors.ts
src/capabilities.ts
file-pattern: 'api/CHANGELOG.md'
skip-label: 'skip api changelog'
failure-message: 'The public API (${prereq-pattern}) was changed without a changelog entry in ${file-pattern} (the ${skip-label} label can be used to pass this check)'
Expand All @@ -71,6 +73,7 @@ jobs:
src/api.ts
src/types.ts
src/publicErrors.ts
src/capabilities.ts
file-pattern: 'docs/README.md'
skip-label: 'skip api docs'
failure-message: 'The public API (${prereq-pattern}) was changed without updating ${file-pattern} (the ${skip-label} label can be used to pass this check)'
10 changes: 3 additions & 7 deletions api/.gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
# Copied from ../src/api.ts, ../src/types.ts, ../src/publicErrors.ts by the publish pipeline
# (build/azure-pipeline.npm.yml). These are the published package sources; they are produced
# at publish time and intentionally NOT committed. src/api.ts, src/types.ts, and
# src/publicErrors.ts are the single sources of truth.
src/main.ts
src/types.ts
src/publicErrors.ts
# Generated package sources copied from ../src by scripts/copy-sources.cjs.
# The original files under ../src are the single sources of truth.
/src/
12 changes: 12 additions & 0 deletions api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to the `@vscode/python-environments` API package are documen
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.6.0]

### Added

- Required `capabilities` maps for environment and package managers, with typed keys, contextual checks, and prerequisite-cycle detection. There are no defaults: a manager must advertise every catalog key explicitly, and an omitted key resolves unsupported.
- Raw-provider capability resolvers and routed extension API queries that report support without invoking manager operations or prompting.
- Capability catalogs for 10 environment and 11 package features.
Comment on lines +12 to +14

### Changed

- `capabilities` is now a required property on `EnvironmentManager` and `PackageManager`. This is a breaking change: existing providers must add a full `capabilities` map before upgrading.

## [1.5.0]

### Added
Expand Down
37 changes: 36 additions & 1 deletion api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,42 @@ export async function activate() {
}
```

## Manager capabilities

Capabilities report general manager support without invoking an operation:

```typescript
const api = await PythonEnvironments.api();
const environment = await api.getEnvironment(undefined);
if (environment && typeof api.getPackageManagerCapability === 'function') {
const support = await api.getPackageManagerCapability(environment, 'packages.direct');
if (support.supported) {
// Best-effort direct/transitive package classification is available.
}
}
```

Managers advertise every capability key explicitly: there are no defaults, and
`capabilities` is a required property on both `EnvironmentManager` and
`PackageManager`. A key a manager omits at runtime resolves
`{ supported: false, reason: 'Capability not implemented' }`.

To change a manager's advertised support, update the check for that key in its
map; return `{ supported: false, reason }` to disable support. Checks must be
read-only and noninteractive.

Capability keys come from the `EnvironmentManagerCapability` and
`PackageManagerCapability` type unions. Adding a public key requires updating
every built-in manager's `capabilities` map, plus tests and documentation.
Removing a key from the type union is a breaking API change.

Feature-detect query methods when supporting older extension runtimes. Queries
use currently registered managers and describe support, not request validity or
guaranteed success.

See the [capability guide](https://github.com/microsoft/vscode-python-environments/blob/main/docs/README.md#manager-capabilities)
for the full catalog and provider maintenance.

## Full API reference

📘 **[Python Environments API reference](https://github.com/microsoft/vscode-python-environments/blob/main/docs/README.md)**
Expand All @@ -51,4 +87,3 @@ extensibility - with field tables, parameter tables, return types, and examples.
- [Extensibility](https://github.com/microsoft/vscode-python-environments/blob/main/docs/README.md#extensibility) - register your own environment manager, package manager, or project creator

See [`CHANGELOG.md`](https://github.com/microsoft/vscode-python-environments/blob/main/api/CHANGELOG.md) for API changes between versions.

4 changes: 2 additions & 2 deletions api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@vscode/python-environments",
"description": "An API facade for the Python Environments extension in VS Code",
"version": "1.5.0",
"version": "1.6.0",
"author": {
"name": "Microsoft Corporation"
},
Expand Down
1 change: 1 addition & 0 deletions api/scripts/copy-sources.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const sources = [
{ from: path.join(repoRoot, 'src', 'api.ts'), to: path.join(srcDir, 'main.ts') },
{ from: path.join(repoRoot, 'src', 'types.ts'), to: path.join(srcDir, 'types.ts') },
{ from: path.join(repoRoot, 'src', 'publicErrors.ts'), to: path.join(srcDir, 'publicErrors.ts') },
{ from: path.join(repoRoot, 'src', 'capabilities.ts'), to: path.join(srcDir, 'capabilities.ts') },
Comment thread
edvilme marked this conversation as resolved.
];

fs.mkdirSync(srcDir, { recursive: true });
Expand Down
1 change: 1 addition & 0 deletions api/scripts/test-package.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ try {
"};",
'exports.__runtimeApi = runtimeApi;',
'exports.extensions = { getExtension: () => extension };',
'exports.l10n = { t: (message) => message };',
].join('\n'),
);
const installedPackageJson = JSON.parse(fs.readFileSync(path.join(installedPackageRoot, 'package.json'), 'utf8'));
Expand Down
153 changes: 117 additions & 36 deletions api/test/consumer.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,140 @@
import type {
Capabilities,
CapabilityContext,
EnvironmentManager,
EnvironmentManagerCapability,
PackageManager,
PackageManagerCapability,
Pep440Version,
PythonEnvironment,
PythonEnvironmentApi,
PythonPackageGetterApi,
Support,
} from '@vscode/python-environments';
import {
isPackageVersionLookupNotSupportedError,
PackageVersionLookupNotSupportedError,
PythonEnvironments,
resolveEnvironmentManagerCapability,
resolvePackageManagerCapability,
supportedCapability,
} from '@vscode/python-environments';

type Equal<Left, Right> =
(<Value>() => Value extends Left ? 1 : 2) extends <Value>() => Value extends Right ? 1 : 2 ? true : false;

// Compile-only fixture shared by modern and legacy consumers; not executed.
declare const api: PythonEnvironmentApi;
declare const environment: PythonEnvironment;
PythonEnvironments.api() satisfies Promise<PythonEnvironmentApi>;

// Package version lookup contracts.
type AvailableVersionsReturn = ReturnType<PythonPackageGetterApi['getPackageAvailableVersions']>;
type RefreshReturn = ReturnType<PackageManager['refresh']>;

const availableVersionsReturnIsExact: Equal<AvailableVersionsReturn, Promise<Pep440Version[] | undefined>> = true;
const refreshReturnIsExact: Equal<RefreshReturn, Promise<void>> = true;
true satisfies Equal<AvailableVersionsReturn, Promise<Pep440Version[] | undefined>>;
true satisfies Equal<RefreshReturn, Promise<void>>;

declare const api: PythonPackageGetterApi;
declare const environment: PythonEnvironment;
const legacyAvailableVersions: Promise<Pep440Version[] | undefined> = api.getPackageAvailableVersions(
environment,
'example',
);
const explicitLegacyAvailableVersions: Promise<Pep440Version[] | undefined> = api.getPackageAvailableVersions(
environment,
'example',
{ errorMode: 'legacy' },
);
const throwingAvailableVersions: Promise<Pep440Version[]> = api.getPackageAvailableVersions(environment, 'example', {
errorMode: 'throw',
});
const runtimeApi: Promise<PythonEnvironmentApi> = PythonEnvironments.api();

// The unsupported-capability error is part of the public contract: it is constructible, extends
// Error, and exposes a stable string-literal `code` discriminator.
api.getPackageAvailableVersions(environment, 'example') satisfies Promise<Pep440Version[] | undefined>;
api.getPackageAvailableVersions(environment, 'example', {
errorMode: 'legacy',
}) satisfies Promise<Pep440Version[] | undefined>;
api.getPackageAvailableVersions(environment, 'example', { errorMode: 'throw' }) satisfies Promise<Pep440Version[]>;

// Public error construction, discriminator, and narrowing.
const lookupError = new PackageVersionLookupNotSupportedError('unsupported');
const lookupErrorIsError: Error = lookupError;
const lookupErrorCodeIsExact: Equal<typeof lookupError.code, 'PackageVersionLookupNotSupported'> = true;
lookupError satisfies Error;
true satisfies Equal<typeof lookupError.code, 'PackageVersionLookupNotSupported'>;

// The type guard narrows unknown values via the stable discriminator (bundle-boundary safe).
declare const maybeError: unknown;
const guardNarrows: boolean = isPackageVersionLookupNotSupportedError(maybeError)
? maybeError.code === 'PackageVersionLookupNotSupported'
: false;

void availableVersionsReturnIsExact;
void refreshReturnIsExact;
void legacyAvailableVersions;
void explicitLegacyAvailableVersions;
void throwingAvailableVersions;
void runtimeApi;
void lookupErrorIsError;
void lookupErrorCodeIsExact;
void guardNarrows;
if (isPackageVersionLookupNotSupportedError(maybeError)) {
maybeError.code satisfies 'PackageVersionLookupNotSupported';
}

// Legacy providers must now advertise every capability key explicitly; there are no defaults.
const fullPackageCapabilities: Capabilities<PackageManagerCapability> = {
'packages.list': supportedCapability,
'packages.refresh': supportedCapability,
'packages.manage': supportedCapability,
'packages.manage.install': supportedCapability,
'packages.manage.uninstall': supportedCapability,
'packages.manage.upgrade': supportedCapability,
'packages.direct': supportedCapability,
'packages.availableVersions': supportedCapability,
};
const fullEnvironmentCapabilities: Capabilities<EnvironmentManagerCapability> = {
'environments.list': supportedCapability,
'environments.refresh': supportedCapability,
'environments.resolve': supportedCapability,
'environments.getSelected': supportedCapability,
'environments.setSelected': supportedCapability,
'environments.create': supportedCapability,
'environments.create.quick': supportedCapability,
'environments.remove': supportedCapability,
};
Comment on lines +65 to +74

const legacyPackageManager: PackageManager = {
name: 'legacy',
manage: async () => {},
refresh: async () => {},
getPackages: async () => [],
capabilities: fullPackageCapabilities,
};
const legacyEnvironmentManager: EnvironmentManager = {
name: 'legacy',
preferredPackageManagerId: 'example:legacy',
refresh: async () => {},
getEnvironments: async () => [],
get: async () => undefined,
set: async () => {},
resolve: async () => undefined,
capabilities: fullEnvironmentCapabilities,
};

// Capability authoring and raw-provider resolution.
const advertised: Capabilities<PackageManagerCapability> = {
...fullPackageCapabilities,
'packages.manage.install': (context: CapabilityContext) =>
resolvePackageManagerCapability(legacyPackageManager, 'packages.manage', context),
'packages.direct': async (_context) => ({ supported: false, reason: 'Unavailable' }),
};
const managerWithCapabilities: PackageManager = { ...legacyPackageManager, capabilities: advertised };
resolvePackageManagerCapability(managerWithCapabilities, 'packages.direct', { environment }) satisfies Promise<Support>;
resolveEnvironmentManagerCapability(legacyEnvironmentManager, 'environments.create', {
scope: 'global',
}) satisfies Promise<Support>;

// Public queries and support-result narrowing.
api.getPackageManagerCapability(environment, 'packages.list') satisfies Promise<Support>;
api.getEnvironmentManagerCapability('example:legacy', 'environments.list', { scope: 'all' }) satisfies Promise<Support>;
declare const support: Support;
if (!support.supported) {
support.reason satisfies string;
}

// Invalid capability keys, advertisements, and context must remain compile errors.
// @ts-expect-error Package keys do not belong to the environment capability domain.
'packages.list' satisfies EnvironmentManagerCapability;
// @ts-expect-error Environment keys do not belong to the package capability domain.
'environments.list' satisfies PackageManagerCapability;
// @ts-expect-error Advertisements must be asynchronous functions, not static Support values.
({ 'packages.list': { supported: true } } satisfies Capabilities<PackageManagerCapability>);
// @ts-expect-error Advertisements cannot introduce arbitrary keys.
({ 'packages.unknown': async () => ({ supported: true }) } satisfies Capabilities<PackageManagerCapability>);
// @ts-expect-error Operation options are not capability query context.
({ createOptions: { quickCreate: true } } satisfies CapabilityContext);
declare const arbitraryKey: string;
// @ts-expect-error A general string is not a declared environment capability.
resolveEnvironmentManagerCapability(legacyEnvironmentManager, arbitraryKey);
// @ts-expect-error A general string is not a declared package capability.
resolvePackageManagerCapability(legacyPackageManager, arbitraryKey);
// @ts-expect-error Public environment queries require a catalog key.
api.getEnvironmentManagerCapability('example:legacy', arbitraryKey);
// @ts-expect-error Public package queries require a catalog key.
api.getPackageManagerCapability(environment, arbitraryKey);
// @ts-expect-error Capability dictionaries have no arbitrary string index signature.
fullPackageCapabilities[arbitraryKey];
// @ts-expect-error Capability dictionaries have no arbitrary string index signature.
fullEnvironmentCapabilities[arbitraryKey];
// @ts-expect-error Capability dictionaries must advertise every catalog key; partial maps are rejected.
({ 'packages.list': async () => ({ supported: true }) } satisfies Capabilities<PackageManagerCapability>);
2 changes: 1 addition & 1 deletion build/azure-pipeline.npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ extends:

- script: npm run copy:sources
workingDirectory: $(Build.SourcesDirectory)/api
displayName: Copy src/api.ts, src/types.ts, src/publicErrors.ts to API package sources
displayName: Copy API facade, types, errors, and capabilities to API package sources

- script: npm run compile
workingDirectory: $(Build.SourcesDirectory)/api
Expand Down
Loading
Loading