Skip to content
Merged
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ dist
node_modules
.vscode-test/
*.vsix
*.tgz
*.tsbuildinfo
.nox/
.venv/
Expand Down
3 changes: 3 additions & 0 deletions api/.npmignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
node_modules/
src/
scripts/
test/
out/**/*.map
*.tgz
*.tsbuildinfo
tsconfig*.json
18 changes: 16 additions & 2 deletions api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ 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.37.0]
## [1.1.0]
Comment thread
edvilme marked this conversation as resolved.

- Aligned the API package version with the Python Environments extension version.
### Added

- Re-exported the `Pep440Version` type from `@renovatebot/pep440` for use with the new package version APIs.
- Added the optional `PackageInfo.isTransitive?: boolean` property to indicate whether a package is a transitive dependency.
- Added `GetPackagesOptions` with an optional `skipCache?: boolean` property. When `true`, package managers bypass cached data and query the underlying package management tool.
- Added optional `PackageManager.getPackageWatchTargets?(environment: PythonEnvironment): RelativePattern[]` to return manager-specific filesystem patterns to monitor for package installation and removal changes, in addition to the default site-packages metadata locations.
- Added optional `PackageManager.getDirectPackageNames?(environment: PythonEnvironment): Promise<Set<string> | undefined>` to return a best-effort set of direct, non-transitive package names when supported by the package manager.
- Added optional `PackageManager.getVersion?(environment: PythonEnvironment): Promise<Pep440Version | undefined>` to return the version of the underlying package management tool, such as pip, uv, or conda.
- Added optional `PackageManager.getPackageAvailableVersions?(environment: PythonEnvironment, packageName: string): Promise<Pep440Version[] | undefined>` to return the available versions of a package in newest-first order when supported.
- Added optional `PackageManager.formatInstallSpec?(packageName: string, version: string): string` to format a versioned install specification using manager-specific syntax, such as `name==version` for pip or `name=version` for conda.
- Added `PythonPackageGetterApi.getPackageAvailableVersions(environment: PythonEnvironment, packageName: string): Promise<Pep440Version[] | undefined>` so API consumers can query a package's available versions in newest-first order. Resolves to `undefined` when the environment's package manager does not support version listing.

### Changed

- Added the optional `options?: GetPackagesOptions` parameter to `PackageManager.getPackages(environment, options?)` and `PythonPackageGetterApi.getPackages(environment, options?)`. Consumers can set `options.skipCache` to request fresh package data.
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.

7 changes: 5 additions & 2 deletions 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.0.0",
"version": "1.1.0",
Comment thread
edvilme marked this conversation as resolved.
"author": {
"name": "Microsoft Corporation"
},
Expand All @@ -11,6 +11,8 @@
"API",
"Environments"
],
"main": "./out/cjs/main.cjs",
"types": "./out/cjs/main.d.ts",
"exports": {
"import": {
"types": "./out/esm/main.d.ts",
Expand Down Expand Up @@ -41,7 +43,8 @@
"compile": "npm run compile:esm && npm run compile:cjs",
"compile:esm": "tsc -b ./tsconfig.esm.json && mve out/esm/main.js out/esm/main.mjs",
"compile:cjs": "tsc -b ./tsconfig.cjs.json && mve out/cjs/main.js out/cjs/main.cjs",
"clean": "node -e \"const fs = require('fs'); fs.rmSync('./out', { recursive: true, force: true });\""
"clean": "node -e \"const fs = require('fs'); fs.rmSync('./out', { recursive: true, force: true });\"",
"test:package": "node ./scripts/test-package.cjs"
},
"devDependencies": {
"@types/node": "^22.0.0",
Expand Down
128 changes: 128 additions & 0 deletions api/scripts/test-package.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

const assert = require('node:assert');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
const { createRequire } = require('node:module');
const { fileURLToPath } = require('node:url');

const packageRoot = path.resolve(__dirname, '..');
const npmCli = process.env.npm_execpath;

if (!npmCli) {
throw new Error('npm_execpath is unavailable. Run this validation through npm run test:package.');
}

function runNodeScript(script, args, cwd, captureOutput = false) {
return execFileSync(process.execPath, [script, ...args], {
cwd,
encoding: 'utf8',
stdio: captureOutput ? ['ignore', 'pipe', 'inherit'] : 'inherit',
});
}

const packOutput = runNodeScript(npmCli, ['pack', '--ignore-scripts', '--json'], packageRoot, true);
const packResult = JSON.parse(packOutput);
assert.strictEqual(packResult.length, 1, 'Expected npm pack to produce exactly one package');

const tarballPath = path.join(packageRoot, packResult[0].filename);
const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'python-environments-api-'));

function canonicalPath(value) {
return fs.realpathSync.native(path.resolve(value));
}

try {
fs.writeFileSync(
path.join(testRoot, 'package.json'),
JSON.stringify({ name: 'python-environments-api-consumer', private: true }),
);

runNodeScript(
npmCli,
[
'install',
'--ignore-scripts',
'--no-package-lock',
'--no-save',
tarballPath,
'@types/node@^22.0.0',
'@types/vscode@^1.99.0',
],
testRoot,
);

const typescriptCli = path.join(packageRoot, 'node_modules', 'typescript', 'bin', 'tsc');
const fixtureRoot = path.join(packageRoot, 'test');

for (const consumer of [
{ name: 'modern', type: 'module' },
{ name: 'legacy', type: 'commonjs' },
]) {
const consumerRoot = path.join(testRoot, consumer.name);
fs.mkdirSync(consumerRoot);
fs.copyFileSync(path.join(fixtureRoot, 'consumer.ts'), path.join(consumerRoot, 'consumer.ts'));
fs.copyFileSync(
path.join(fixtureRoot, `tsconfig.${consumer.name}.json`),
path.join(consumerRoot, 'tsconfig.json'),
);
fs.writeFileSync(
path.join(consumerRoot, 'package.json'),
JSON.stringify({ private: true, type: consumer.type }),
);

runNodeScript(typescriptCli, ['--project', path.join(consumerRoot, 'tsconfig.json')], packageRoot);
}

const installedPackageRoot = path.join(testRoot, 'node_modules', '@vscode', 'python-environments');
const installedPackageJson = JSON.parse(fs.readFileSync(path.join(installedPackageRoot, 'package.json'), 'utf8'));
assert.strictEqual(installedPackageJson.main, './out/cjs/main.cjs');
assert.strictEqual(installedPackageJson.types, './out/cjs/main.d.ts');
assert.deepStrictEqual(installedPackageJson.exports, {
import: {
types: './out/esm/main.d.ts',
default: './out/esm/main.mjs',
},
require: {
types: './out/cjs/main.d.ts',
default: './out/cjs/main.cjs',
},
});

for (const target of [
installedPackageJson.main,
installedPackageJson.types,
installedPackageJson.exports.import.types,
installedPackageJson.exports.import.default,
installedPackageJson.exports.require.types,
installedPackageJson.exports.require.default,
]) {
assert.ok(fs.statSync(path.resolve(installedPackageRoot, target)).isFile(), `${target} must be a file`);
}

const requireFromConsumer = createRequire(path.join(testRoot, 'legacy', 'consumer.cjs'));
assert.strictEqual(
canonicalPath(requireFromConsumer.resolve('@vscode/python-environments')),
canonicalPath(path.join(installedPackageRoot, installedPackageJson.exports.require.default)),
'CommonJS consumers should resolve the packaged CommonJS entry point',
);

const esmEntryPoint = execFileSync(
process.execPath,
['--input-type=module', '--eval', "console.log(import.meta.resolve('@vscode/python-environments'))"],
{
cwd: path.join(testRoot, 'modern'),
encoding: 'utf8',
},
).trim();
assert.strictEqual(
canonicalPath(fileURLToPath(esmEntryPoint)),
canonicalPath(path.join(installedPackageRoot, installedPackageJson.exports.import.default)),
'ES module consumers should resolve the packaged ES module entry point',
);
} finally {
fs.rmSync(testRoot, { recursive: true, force: true });
}
23 changes: 23 additions & 0 deletions api/test/consumer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type {
PackageManager,
Pep440Version,
PythonEnvironment,
PythonPackageGetterApi,
} from '@vscode/python-environments';

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

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;

declare const api: PythonPackageGetterApi;
declare const environment: PythonEnvironment;
const availableVersions: Promise<Pep440Version[] | undefined> = api.getPackageAvailableVersions(environment, 'example');

void availableVersionsReturnIsExact;
void refreshReturnIsExact;
void availableVersions;
11 changes: 11 additions & 0 deletions api/test/tsconfig.legacy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "Node",
"noEmit": true,
"strict": true,
"skipLibCheck": false
},
"include": ["consumer.ts"]
}
11 changes: 11 additions & 0 deletions api/test/tsconfig.modern.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"noEmit": true,
"strict": true,
"skipLibCheck": false
},
"include": ["consumer.ts"]
}
1 change: 1 addition & 0 deletions api/tsconfig.esm.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"outDir": "./out/esm"
}
}
6 changes: 3 additions & 3 deletions build/azure-pipeline.npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,17 @@ extends:
workingDirectory: $(Build.SourcesDirectory)/api
displayName: Install package dependencies

- script: cp ../src/api.ts src/main.ts
- script: mkdir -p src && cp ../src/api.ts src/main.ts
workingDirectory: $(Build.SourcesDirectory)/api
displayName: Copy src/api.ts to API package entry point

- script: npm run compile
workingDirectory: $(Build.SourcesDirectory)/api
displayName: Compile TypeScript

- script: npm pack --ignore-scripts
- script: npm run test:package
workingDirectory: $(Build.SourcesDirectory)/api
displayName: Pack npm package
displayName: Pack and validate npm package

- task: CopyFiles@2
displayName: Copy package tarball to staging
Expand Down
24 changes: 20 additions & 4 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,9 +679,9 @@ export interface PackageManager {
/**
* Refreshes the package list for the specified Python environment.
* @param environment - The Python environment for which to refresh the package list.
* @returns A promise that resolves with the refreshed list of packages, or undefined.
* @returns A promise that resolves when the refresh is complete.
*/
refresh(environment: PythonEnvironment): Promise<Package[] | undefined>;
refresh(environment: PythonEnvironment): Promise<void>;

/**
* Retrieves the list of packages for the specified Python environment.
Expand Down Expand Up @@ -1095,9 +1095,9 @@ export interface PythonPackageGetterApi {
* Refresh the list of packages in a Python Environment.
*
* @param environment The Python Environment for which the list of packages is to be refreshed.
* @returns A promise that resolves with the refreshed list of packages, or undefined.
* @returns A promise that resolves when the list of packages has been refreshed.
*/
refreshPackages(environment: PythonEnvironment): Promise<Package[] | undefined>;
refreshPackages(environment: PythonEnvironment): Promise<void>;

/**
* Get the list of packages in a Python Environment.
Expand All @@ -1108,6 +1108,22 @@ export interface PythonPackageGetterApi {
*/
getPackages(environment: PythonEnvironment, options?: GetPackagesOptions): Promise<Package[] | undefined>;

/**
* Get the list of available versions for a package, newest first.
*
* Support depends on the package manager backing the environment. Managers that do
* not implement version lookup resolve to `undefined`.
*
* @param environment The Python Environment context for the lookup.
* @param packageName The name of the package to look up.
* @returns A promise that resolves to an array of {@link Pep440Version} objects (newest first),
* or `undefined` if the package manager does not support version listing.
*/
getPackageAvailableVersions(
environment: PythonEnvironment,
packageName: string,
): Promise<Pep440Version[] | undefined>;

/**
* Event raised when the list of packages in a Python Environment changes.
* @see {@link DidChangePackagesEventArgs}
Expand Down
14 changes: 13 additions & 1 deletion src/features/pythonApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
PackageInfo,
PackageManagementOptions,
PackageManager,
Pep440Version,
PythonBackgroundRunOptions,
PythonEnvironment,
PythonEnvironmentApi,
Expand Down Expand Up @@ -303,7 +304,7 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
}
return manager.manage(context, options);
}
async refreshPackages(context: PythonEnvironment): Promise<Package[] | undefined> {
async refreshPackages(context: PythonEnvironment): Promise<void> {
await waitForEnvManagerId([context.envId.managerId]);
const manager = this.envManagers.getPackageManager(context);
if (!manager) {
Expand All @@ -319,6 +320,17 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
}
Comment thread
edvilme marked this conversation as resolved.
return manager.getPackages(context, options);
}
async getPackageAvailableVersions(
context: PythonEnvironment,
packageName: string,
): Promise<Pep440Version[] | undefined> {
await waitForEnvManagerId([context.envId.managerId]);
const manager = this.envManagers.getPackageManager(context);
if (!manager) {
return Promise.resolve(undefined);
}
return manager.getPackageAvailableVersions(context, packageName);
}
onDidChangePackages: Event<DidChangePackagesEventArgs> = this._onDidChangePackages.event;

createPackageItem(info: PackageInfo, environment: PythonEnvironment, manager: PackageManager): Package {
Expand Down
3 changes: 2 additions & 1 deletion src/features/views/envManagersView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ export class EnvManagerView implements TreeDataProvider<EnvTreeItem>, Disposable
const views: EnvTreeItem[] = [];

if (pkgManager) {
let packages = await pkgManager.refresh(environment);
await pkgManager.refresh(environment);
const packages = await pkgManager.getPackages(environment);
if (packages && packages.length > 0) {
views.push(
...packages
Expand Down
3 changes: 2 additions & 1 deletion src/features/views/projectView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,8 @@ export class ProjectView implements TreeDataProvider<ProjectTreeItem> {
return [new ProjectEnvironmentInfo(environmentItem, ProjectViews.noPackageManager)];
}

let packages = await pkgManager.refresh(environment);
await pkgManager.refresh(environment);
const packages = await pkgManager.getPackages(environment);
if (!packages) {
return [new ProjectEnvironmentInfo(environmentItem, ProjectViews.noPackages)];
}
Expand Down
2 changes: 1 addition & 1 deletion src/internal.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ export class InternalPackageManager implements PackageManager {
}
}

refresh(environment: PythonEnvironment): Promise<Package[] | undefined> {
refresh(environment: PythonEnvironment): Promise<void> {
return this.manager.refresh(environment);
}

Expand Down
Loading
Loading