Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
a44fb1b
API: Advertise EnvironmentManager and PackageManager capabilities
edvilme Oct 2, 2026
230433f
Update default values
edvilme Oct 2, 2026
7a1ef69
Consolidate capabilities and default values into single source of truth
edvilme Oct 2, 2026
02b937a
Rename to PackageManagerCapability and EnvironmentManagerCapability
edvilme Oct 2, 2026
6853e32
Defer manager capability runtime preflight validation
edvilme Oct 2, 2026
ca37bf0
Simplify tests
edvilme Oct 2, 2026
5704195
Use only registererd managers
edvilme Oct 2, 2026
c06cc48
Simplify tests
edvilme Oct 2, 2026
acab878
Ground tests
edvilme Oct 2, 2026
0535149
Address manager capability review feedback
edvilme Oct 2, 2026
4102baa
Reduce public exports
edvilme Oct 2, 2026
ab06d1a
fix: harden manager capability resolution
edvilme Oct 2, 2026
0fe8c1b
docs: simplify manager capability guidance
edvilme Oct 2, 2026
e436017
Remove default values
edvilme Oct 8, 2026
93a2815
Add capability supported helper methods
edvilme Oct 8, 2026
a15f7f4
Simplify implementation
edvilme Oct 8, 2026
fafcf46
Update test
edvilme Oct 8, 2026
64e88a3
Simplify test
edvilme Oct 8, 2026
f485101
Fix tests that did not exercise their intended behavior
edvilme Oct 8, 2026
e02e7f3
Remove capability tests that duplicate existing package-manager routi…
edvilme Oct 8, 2026
e7b6dc3
Remove registry lookup error test that only verified default throw pr…
edvilme Oct 8, 2026
3bf34fe
Simplify capabilities.ts
edvilme Oct 8, 2026
c9ddfdf
Update README
edvilme Oct 8, 2026
957e35b
Include capabilities in public API change checks
Copilot Oct 8, 2026
e21580b
Add required capabilities to mock managers after rebase onto main
edvilme Oct 9, 2026
5449b6b
Probe pip/uv version lookup support via getPackageAvailableVersionsCo…
edvilme Oct 9, 2026
9f7601f
Remove environments.create.quick capability key
edvilme Oct 9, 2026
3f1a342
Send telemetry
edvilme Oct 12, 2026
2dfc20d
Redefine capabilities and anmes
edvilme Oct 12, 2026
7e8370b
Don't use string keys
edvilme Oct 12, 2026
bf8bfff
Address PR comments
edvilme Oct 12, 2026
d0aeaf9
API: Advertise EnvironmentManager and PackageManager capabilities
edvilme Oct 12, 2026
07b1e07
Bump api version
edvilme Oct 12, 2026
e4bebfd
Add legacyCapability wrappers to avoid breaking behavior for now
edvilme Oct 12, 2026
ed13a15
Align capability tests and docs with legacy fallback
edvilme Oct 12, 2026
a8f56a1
Keep capability telemetry version lookup off the query path
edvilme Oct 12, 2026
33b599f
Align pip management capabilities with Python 2 restriction
edvilme Oct 12, 2026
1b84fd5
Simplify captureCapabilityQuery
edvilme Oct 12, 2026
dcae6df
Reject unregistered projects in package capability queries
edvilme Oct 12, 2026
7e5b0cc
Updated legacy capability redirection and localization issues
edvilme Oct 12, 2026
dd9ed1f
Simplify tests
edvilme Oct 12, 2026
05da1d3
Address PR comments
edvilme Oct 12, 2026
7720a8e
Update comments
edvilme Oct 13, 2026
53b7c52
Resolve equivalent file project paths in capability queries
Copilot Oct 13, 2026
301d4c9
Fix capabilities names
edvilme Oct 13, 2026
9778899
Update docs
edvilme Oct 14, 2026
f09a2a9
Update docs
edvilme Oct 14, 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
6 changes: 6 additions & 0 deletions .github/workflows/pr-file-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ jobs:
src/api.ts
src/types.ts
src/publicErrors.ts
src/capabilities.ts
src/legacyCapabilities.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 +62,8 @@ jobs:
src/api.ts
src/types.ts
src/publicErrors.ts
src/capabilities.ts
src/legacyCapabilities.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 +75,8 @@ jobs:
src/api.ts
src/types.ts
src/publicErrors.ts
src/capabilities.ts
src/legacyCapabilities.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/
15 changes: 15 additions & 0 deletions api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ 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.7.0]

### Added

- Optional `capabilities` maps for environment and package managers, with typed keys and contextual checks. Omitted maps use legacy method inference; when present, the map must advertise every catalog key explicitly, and missing keys resolve unsupported.
- Raw-provider capability resolvers and routed extension API queries that report support without invoking manager operations or prompting.
- Capability catalogs for six environment and six package features, with flat operation names. Listing includes refresh, and package installation, uninstallation, and upgrades are advertised individually rather than through a management umbrella.
- Exported `EnvironmentCapability` and `PackageCapability` constant objects for named access to capability keys, with derived string-literal unions.
- `getLegacyPackageManagerCapability` and `getLegacyEnvironmentManagerCapability` helpers that infer support from method presence without invoking operations. Resolvers use them automatically when a provider omits its capability map.
- Optional `Support.isLegacy` metadata, set to true by legacy inference helpers and included in public capability-query telemetry.

### Notes

- Capability prerequisite checks have no cycle protection; providers must keep delegation chains acyclic.

## [1.6.0]

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

## Manager capabilities

Capabilities report general manager support without invoking an operation:

```typescript
import { PackageCapability, PythonEnvironments } from '@vscode/python-environments';

const api = await PythonEnvironments.api();
const environment = await api.getEnvironment(undefined);
if (environment) {
const support = await api.getPackageManagerCapability(environment, PackageCapability.ListDirect);
if (support.supported) {
// Best-effort direct/transitive package classification is available.
}
}
```

Manager advertisements are optional for legacy compatibility. Missing maps use
method-presence inference and return `isLegacy: true`; missing keys in a supplied
map report unsupported. Support is advisory, not a guarantee that an operation succeeds.

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

## Full API reference

📘 **[Python Environments API reference](https://github.com/microsoft/vscode-python-environments/blob/main/docs/README.md)**
Expand All @@ -51,4 +75,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.6.0",
"version": "1.7.0",
"author": {
"name": "Microsoft Corporation"
},
Expand Down
2 changes: 2 additions & 0 deletions api/scripts/copy-sources.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ 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.
{ from: path.join(repoRoot, 'src', 'legacyCapabilities.ts'), to: path.join(srcDir, 'legacyCapabilities.ts') },
];

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
161 changes: 125 additions & 36 deletions api/test/consumer.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,148 @@
import type {
Capabilities,
CapabilityContext,
EnvironmentManager,
EnvironmentManagerCapability,
PackageManager,
PackageManagerCapability,
Pep440Version,
PythonEnvironment,
PythonEnvironmentApi,
PythonPackageGetterApi,
Support,
} from '@vscode/python-environments';
import {
EnvironmentCapability,
getLegacyEnvironmentManagerCapability,
getLegacyPackageManagerCapability,
isPackageVersionLookupNotSupportedError,
PackageCapability,
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 may omit advertisements; advertised maps must include every key without defaults.
true satisfies Equal<typeof PackageCapability.ListDirect, 'packages.list.direct'>;
true satisfies Equal<typeof PackageCapability.FetchAvailableVersions, 'packages.fetchversions'>;
const fullPackageCapabilities: Capabilities<PackageManagerCapability> = {
[PackageCapability.List]: supportedCapability,
[PackageCapability.Install]: supportedCapability,
[PackageCapability.Uninstall]: supportedCapability,
[PackageCapability.Upgrade]: supportedCapability,
[PackageCapability.ListDirect]: supportedCapability,
[PackageCapability.FetchAvailableVersions]: supportedCapability,
};
const fullEnvironmentCapabilities: Capabilities<EnvironmentManagerCapability> = {
[EnvironmentCapability.List]: supportedCapability,
[EnvironmentCapability.Resolve]: supportedCapability,
[EnvironmentCapability.GetSelected]: supportedCapability,
[EnvironmentCapability.SetSelected]: supportedCapability,
[EnvironmentCapability.Create]: supportedCapability,
[EnvironmentCapability.Remove]: supportedCapability,
};

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

getLegacyPackageManagerCapability(legacyPackageManager, PackageCapability.Install) satisfies Promise<Support>;
getLegacyEnvironmentManagerCapability(legacyEnvironmentManager, EnvironmentCapability.Create) satisfies Promise<Support>;

// Capability authoring and raw-provider resolution.
const advertised: Capabilities<PackageManagerCapability> = {
...fullPackageCapabilities,
[PackageCapability.Upgrade]: (context: CapabilityContext) =>
resolvePackageManagerCapability(legacyPackageManager, PackageCapability.Install, context),
[PackageCapability.ListDirect]: async (_context) => ({ supported: false, reason: 'Unavailable' }),
};
const managerWithCapabilities: PackageManager = { ...legacyPackageManager, capabilities: advertised };
resolvePackageManagerCapability(managerWithCapabilities, PackageCapability.ListDirect, { environment }) satisfies Promise<Support>;
resolveEnvironmentManagerCapability(legacyEnvironmentManager, EnvironmentCapability.Create, {
scope: 'global',
}) satisfies Promise<Support>;

// Public queries and support-result narrowing.
api.getPackageManagerCapability(environment, PackageCapability.List) satisfies Promise<Support>;
api.getEnvironmentManagerCapability('example:legacy', EnvironmentCapability.List, { scope: 'all' }) satisfies Promise<Support>;
declare const support: Support;
support.isLegacy satisfies boolean | undefined;
if (!support.supported) {
support.reason satisfies string;
}

// Invalid capability keys, advertisements, and context must remain compile errors.
// @ts-expect-error Legacy environment inference requires an environment capability.
getLegacyEnvironmentManagerCapability(legacyEnvironmentManager, PackageCapability.List);
// @ts-expect-error Legacy package inference requires a package capability.
getLegacyPackageManagerCapability(legacyPackageManager, EnvironmentCapability.List);
// @ts-expect-error Package keys do not belong to the environment capability domain.
PackageCapability.List satisfies EnvironmentManagerCapability;
// @ts-expect-error Environment keys do not belong to the package capability domain.
EnvironmentCapability.List satisfies PackageManagerCapability;
// @ts-expect-error Advertisements must be asynchronous functions, not static Support values.
({ [PackageCapability.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.
({ [PackageCapability.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