diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index aedaa39a..01b3e887 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -8,3 +8,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.37.0] - Aligned the API package version with the Python Environments extension version. +- Added `getPackageManager` to retrieve the registered package manager for an environment. diff --git a/src/api.ts b/src/api.ts index 17e18669..a4cc7a00 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1091,6 +1091,14 @@ export interface PythonPackageManagerRegistrationApi { } export interface PythonPackageGetterApi { + /** + * Get the registered package manager associated with a Python Environment. + * + * @param environment The Python Environment whose package manager is required. + * @returns The registered package manager, or undefined if no package manager is available. + */ + getPackageManager(environment: PythonEnvironment): Promise; + /** * Refresh the list of packages in a Python Environment. * diff --git a/src/features/pythonApi.ts b/src/features/pythonApi.ts index 7156cc5f..39f1ca08 100644 --- a/src/features/pythonApi.ts +++ b/src/features/pythonApi.ts @@ -303,6 +303,10 @@ export class PythonEnvironmentApiImpl implements PythonEnvironmentApi { } return manager.manage(context, options); } + async getPackageManager(context: PythonEnvironment): Promise { + await waitForEnvManagerId([context.envId.managerId]); + return this.envManagers.getPackageManager(context); + } async refreshPackages(context: PythonEnvironment): Promise { await waitForEnvManagerId([context.envId.managerId]); const manager = this.envManagers.getPackageManager(context); diff --git a/src/test/integration/packageManager.integration.test.ts b/src/test/integration/packageManager.integration.test.ts new file mode 100644 index 00000000..966196b2 --- /dev/null +++ b/src/test/integration/packageManager.integration.test.ts @@ -0,0 +1,124 @@ +import * as vscode from 'vscode'; + +import assert from 'assert'; +import { PythonEnvironment, PythonEnvironmentApi } from '../../api'; +import { CONDA_MANAGER_ID, VENV_MANAGER_ID } from '../../common/constants'; +import { ENVS_EXTENSION_ID } from '../constants'; +import { waitForCondition } from '../testUtils'; + +const profiles = [ + { + environmentManagerId: VENV_MANAGER_ID, + environmentDirectory: '.venv', + name: 'Pip', + }, + { + environmentManagerId: CONDA_MANAGER_ID, + environmentDirectory: '.conda', + name: 'Conda', + }, +]; + +async function deleteEnvironmentDirectory(uri: vscode.Uri): Promise { + try { + await vscode.workspace.fs.delete(uri, { recursive: true, useTrash: false }); + } catch (error) { + if (!(error instanceof vscode.FileSystemError) || error.code !== 'FileNotFound') { + throw error; + } + } +} + +for (const profile of profiles) { + suite(`${profile.name} Package Manager`, function () { + this.timeout(300_000); + + let api: PythonEnvironmentApi; + let environment: PythonEnvironment | undefined; + let workspaceUri: vscode.Uri; + let previousDefaultEnvManager: string | undefined; + let defaultEnvManagerUpdated = false; + let previousAlwaysUseUv: boolean | undefined; + let alwaysUseUvUpdated = false; + suiteSetup(async function () { + const extension = vscode.extensions.getExtension(ENVS_EXTENSION_ID); + assert.ok(extension, 'Extension not found'); + if (!extension.isActive) { + await extension.activate(); + await waitForCondition(() => extension.isActive, 20_000, 'Extension did not activate in time'); + } + api = extension.exports; + assert.ok(api, 'API not available'); + + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + assert.ok(workspaceFolder, 'Integration test workspace not found'); + workspaceUri = workspaceFolder.uri; + const config = vscode.workspace.getConfiguration('python-envs', workspaceUri); + previousDefaultEnvManager = config.inspect('defaultEnvManager')?.workspaceValue; + await config.update( + 'defaultEnvManager', + profile.environmentManagerId, + vscode.ConfigurationTarget.Workspace, + ); + defaultEnvManagerUpdated = true; + + if (profile.environmentManagerId === VENV_MANAGER_ID) { + previousAlwaysUseUv = config.inspect('alwaysUseUv')?.globalValue; + await config.update('alwaysUseUv', false, vscode.ConfigurationTarget.Global); + alwaysUseUvUpdated = true; + } + + const environmentDirectory = vscode.Uri.joinPath(workspaceUri, profile.environmentDirectory); + await deleteEnvironmentDirectory(environmentDirectory); + await api.refreshEnvironments(workspaceUri); + + environment = await api.createEnvironment(workspaceUri, { quickCreate: true }); + if (!environment) { + this.skip(); + return; + } + assert.strictEqual( + environment.envId.managerId, + profile.environmentManagerId, + `Expected an environment created by ${profile.environmentManagerId}`, + ); + }); + + test(`${profile.name} Package Manager should install, list, and uninstall a package`, async () => { + await api.managePackages(environment!, { install: ['requests'] }); + let packages = await api.getPackages(environment!, { skipCache: true }); + assert.ok(packages?.some((pkg) => pkg.name === 'requests'), 'Package not installed'); + + await api.managePackages(environment!, { uninstall: ['requests'] }); + packages = await api.getPackages(environment!, { skipCache: true }); + assert.ok(!packages?.some((pkg) => pkg.name === 'requests'), 'Package not uninstalled'); + }); + + test(`${profile.name} Package Manager should list available package versions`, async () => { + const packageManager = await api.getPackageManager(environment!); + assert.ok(packageManager, 'Package manager not available'); + assert.ok(packageManager.getPackageAvailableVersions, 'Available versions method not available'); + + const versions = await packageManager.getPackageAvailableVersions(environment!, 'requests'); + assert.ok(versions, 'Package versions not available'); + assert.ok(versions.length > 0, 'No package versions available'); + }); + + suiteTeardown(async () => { + try { + await deleteEnvironmentDirectory(vscode.Uri.joinPath(workspaceUri, profile.environmentDirectory)); + } finally { + if (alwaysUseUvUpdated) { + await vscode.workspace + .getConfiguration('python-envs') + .update('alwaysUseUv', previousAlwaysUseUv, vscode.ConfigurationTarget.Global); + } + if (defaultEnvManagerUpdated) { + await vscode.workspace + .getConfiguration('python-envs', workspaceUri) + .update('defaultEnvManager', previousDefaultEnvManager, vscode.ConfigurationTarget.Workspace); + } + } + }); + }); +}