From 8b0a87f970b08c78315016fd0d9854decbd6a986 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 20 Aug 2025 15:10:18 -0700 Subject: [PATCH 1/5] initial scaffolding (#741) scaffolding for pipenv as an env manager --- src/common/localize.ts | 6 ++ src/managers/pipenv/main.ts | 27 ++++++++ src/managers/pipenv/pipenvManager.ts | 93 ++++++++++++++++++++++++++++ src/managers/pipenv/pipenvUtils.ts | 11 ++++ 4 files changed, 137 insertions(+) create mode 100644 src/managers/pipenv/main.ts create mode 100644 src/managers/pipenv/pipenvManager.ts create mode 100644 src/managers/pipenv/pipenvUtils.ts diff --git a/src/common/localize.ts b/src/common/localize.ts index 3caa8a9b..1674a397 100644 --- a/src/common/localize.ts +++ b/src/common/localize.ts @@ -156,6 +156,12 @@ export namespace PyenvStrings { export const pyenvRefreshing = l10n.t('Refreshing Pyenv Python versions'); } +export namespace PipenvStrings { + export const pipenvManager = l10n.t('Manages Pipenv environments'); + export const pipenvDiscovering = l10n.t('Discovering Pipenv environments'); + export const pipenvRefreshing = l10n.t('Refreshing Pipenv environments'); +} + export namespace PoetryStrings { export const poetryManager = l10n.t('Manages Poetry environments'); export const poetryDiscovering = l10n.t('Discovering Poetry environments'); diff --git a/src/managers/pipenv/main.ts b/src/managers/pipenv/main.ts new file mode 100644 index 00000000..b49affc5 --- /dev/null +++ b/src/managers/pipenv/main.ts @@ -0,0 +1,27 @@ +import { Disposable } from 'vscode'; +import { PythonEnvironmentApi } from '../../api'; +import { traceInfo } from '../../common/logging'; +import { getPythonApi } from '../../features/pythonApi'; +import { NativePythonFinder } from '../common/nativePythonFinder'; +import { PipenvManager } from './pipenvManager'; +import { getPipenv } from './pipenvUtils'; + +export async function registerPipenvFeatures( + nativeFinder: NativePythonFinder, + disposables: Disposable[], +): Promise { + const api: PythonEnvironmentApi = await getPythonApi(); + + try { + const pipenv = await getPipenv(nativeFinder); + + if (pipenv) { + const mgr = new PipenvManager(nativeFinder, api); + disposables.push(mgr, api.registerEnvironmentManager(mgr)); + } else { + traceInfo('Pipenv not found, turning off pipenv features.'); + } + } catch (ex) { + traceInfo('Pipenv not found, turning off pipenv features.', ex); + } +} diff --git a/src/managers/pipenv/pipenvManager.ts b/src/managers/pipenv/pipenvManager.ts new file mode 100644 index 00000000..6883a365 --- /dev/null +++ b/src/managers/pipenv/pipenvManager.ts @@ -0,0 +1,93 @@ +import { EventEmitter, MarkdownString } from 'vscode'; +import { + CreateEnvironmentOptions, + CreateEnvironmentScope, + DidChangeEnvironmentEventArgs, + DidChangeEnvironmentsEventArgs, + EnvironmentManager, + GetEnvironmentScope, + GetEnvironmentsScope, + IconPath, + PythonEnvironment, + PythonEnvironmentApi, + QuickCreateConfig, + RefreshEnvironmentsScope, + ResolveEnvironmentContext, + SetEnvironmentScope, +} from '../../api'; +import { PipenvStrings } from '../../common/localize'; +import { NativePythonFinder } from '../common/nativePythonFinder'; + +export class PipenvManager implements EnvironmentManager { + private collection: PythonEnvironment[] = []; + private fsPathToEnv: Map = new Map(); + private globalEnv: PythonEnvironment | undefined; + + private readonly _onDidChangeEnvironment = new EventEmitter(); + public readonly onDidChangeEnvironment = this._onDidChangeEnvironment.event; + + private readonly _onDidChangeEnvironments = new EventEmitter(); + public readonly onDidChangeEnvironments = this._onDidChangeEnvironments.event; + constructor(private readonly nativeFinder: NativePythonFinder, private readonly api: PythonEnvironmentApi) { + this.name = 'pipenv'; + this.displayName = 'Pipenv'; + this.preferredPackageManagerId = 'ms-python.python:pip'; + this.tooltip = new MarkdownString(PipenvStrings.pipenvManager, true); + } + + name: string; + displayName: string; + preferredPackageManagerId: string; + description?: string; + tooltip: string | MarkdownString; + iconPath?: IconPath; + + public dispose() { + this.collection = []; + this.fsPathToEnv.clear(); + } + + quickCreateConfig?(): QuickCreateConfig | undefined { + // To be implemented + return undefined; + } + + async create?( + _scope: CreateEnvironmentScope, + _options?: CreateEnvironmentOptions, + ): Promise { + // To be implemented + return undefined; + } + + async remove?(_environment: PythonEnvironment): Promise { + // To be implemented + } + + async refresh(_scope: RefreshEnvironmentsScope): Promise { + // To be implemented + } + + async getEnvironments(_scope: GetEnvironmentsScope): Promise { + // To be implemented + return []; + } + + async set(_scope: SetEnvironmentScope, _environment?: PythonEnvironment): Promise { + // To be implemented + } + + async get(_scope: GetEnvironmentScope): Promise { + // To be implemented + return undefined; + } + + async resolve(_context: ResolveEnvironmentContext): Promise { + // To be implemented + return undefined; + } + + async clearCache?(): Promise { + // To be implemented + } +} diff --git a/src/managers/pipenv/pipenvUtils.ts b/src/managers/pipenv/pipenvUtils.ts new file mode 100644 index 00000000..48997450 --- /dev/null +++ b/src/managers/pipenv/pipenvUtils.ts @@ -0,0 +1,11 @@ +// Utility functions for Pipenv environment management + +import { NativePythonFinder } from '../common/nativePythonFinder'; + +export class PipenvUtils { + // Add static helper methods for pipenv operations here +} +export async function getPipenv(_native?: NativePythonFinder): Promise { + // Implementation to find and return the pipenv path + return undefined; +} From 414098a991a518902f48edb30008d5323ce29829 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 21 Aug 2025 15:56:41 -0700 Subject: [PATCH 2/5] Add env manager pipenv (#749) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- docs/pipenv-integration.md | 74 ++++++ src/extension.ts | 2 + src/managers/pipenv/main.ts | 10 +- src/managers/pipenv/pipenvManager.ts | 249 ++++++++++++++++++-- src/managers/pipenv/pipenvPackageManager.ts | 73 ++++++ src/managers/pipenv/pipenvUtils.ts | 245 ++++++++++++++++++- 6 files changed, 625 insertions(+), 28 deletions(-) create mode 100644 docs/pipenv-integration.md create mode 100644 src/managers/pipenv/pipenvPackageManager.ts diff --git a/docs/pipenv-integration.md b/docs/pipenv-integration.md new file mode 100644 index 00000000..0d6a5bab --- /dev/null +++ b/docs/pipenv-integration.md @@ -0,0 +1,74 @@ +# Pipenv Environment Manager — Implementation Plan + +Summary +- This doc lists the tasks required to get pipenv integrated as an EnvironmentManager + +Files & methods to implement +1. Registration/activation (src/managers/pipenv/main.ts) + - Implement `registerPipenvFeatures(nativeFinder: NativePythonFinder, disposables: Disposable[])`. + - Use the same registration pattern as `pyenv` and `poetry` (get Python API, detect pipenv, instantiate manager, `api.registerEnvironmentManager(mgr)`, push disposables). + +2. Utilities (src/managers/pipenv/pipenvUtils.ts) + - `getPipenv(native?: NativePythonFinder)` — locate the `pipenv` binary (persisted override, env vars, which, or `nativeFinder` fallback). + - `refreshPipenv(hardRefresh, nativeFinder, api, manager)` — discover pipenv environments (workspace Pipfiles, native finder info, or scanning `WORKON_HOME`). + - `resolvePipenvPath(fsPath, nativeFinder, api, manager)` — resolve a path or Uri to a PythonEnvironment. + - `nativeToPythonEnv(nativeInfo, api, manager, pipenvPath)` — convert native discovery info into a `PythonEnvironment`: + - set `execInfo.run.executable` (use `pipenv --py` when possible) and `execInfo.shellActivation` (see activation strategy below), `sysPrefix`, display metadata. + +3. Manager implementation (src/managers/pipenv/pipenvManager.ts) + - Implement class like `PoetryManager`/`PyEnvManager`: + - fields: `collection: PythonEnvironment[]`, `fsPathToEnv: Map`, `globalEnv`. + - event emitters: `_onDidChangeEnvironments`, `_onDidChangeEnvironment` and public events. + - constructor(nativeFinder, api) and metadata properties (`name`, `displayName`, `preferredPackageManagerId`, `tooltip`). + - lifecycle methods: `initialize()`, `getEnvironments()`, `refresh()`, `get()`, `set()`, `resolve()`, `clearCache()`. + - helpers: `loadEnvMap()`, `fromEnvMap(uri)`, `findEnvironmentByPath(fsPath)`. + - Use `api.createPythonEnvironmentItem()` to create `PythonEnvironment` items. + +4. Exec info & activation behavior + - Resolve a Python executable using `pipenv --py` when possible and set `execInfo.run.executable`. + - Activation options: + - Provide `shellActivation` mapping with `'unknown'` fallback. For example `{ executable: 'pipenv', args: ['shell'] }` for activation. + - Provide `activatedRun` or `run` that uses resolved python (`/path/to/venv/bin/python`) or fallback to `pipenv run python` (e.g., run.executable = 'pipenv', args = ['run', 'python']). + - Set `shellDeactivation` to `exit`/`deactivate` where appropriate. + +5. Workspace mapping & persistence + - Implement per-workspace persistent selection (get/set persisted environment for workspace & global), similar to `pyenv` and `poetry` utils. + - Implement logic in `loadEnvMap()` to pick project-specific envs (Pipfile location), global fallback, and mapping to projects via `api.getPythonProjects()`. + +6. Package-manager (required) + - Implement a dedicated Pipenv `PackageManager` and register it via `api.registerPackageManager(...)`. + - Use package manager id: `ms-python.python:pipenv`. + - Implement install/uninstall by invoking `pipenv install`/`pipenv uninstall` and firing package-change events. + +7. Tests + - Add unit tests (mocking `NativePythonFinder` and `getPythonApi`) for detection, discovery, `resolve()` and mapping. + - Add integration tests that run `pipenv --py`/`pipenv --venv` behavior using a test fixture if desired. + +8. Localization & assets + - Add localized strings (e.g., `PipenvStrings`) for messages and progress titles. + - Add icon(s) if required and reference via `iconPath`. + +9. Documentation + - Update README/docs to include Pipenv support and configuration/setting notes. + +10. CI & linting + - Run tests and fix TypeScript compile/lint issues (unused args, correct imports). Ensure `main.ts` registration uses `api.registerEnvironmentManager` like other managers. + +Minimal viable implementation (priority) +1. Fix `main.ts` to implement `registerPipenvFeatures(...)` and register the manager (so the manager is known to the extension). +2. Implement `getPipenv()` (detect pipenv binary) and `nativeToPythonEnv()` (at minimum obtain python path using `pipenv --py` and return a valid `PythonEnvironment` via `api.createPythonEnvironmentItem`). +3. Implement manager skeleton (constructor, event emitters, `initialize()`, `getEnvironments()` and `resolve()` that uses utils above) and wire registration. +4. Add a simple integration test and run the extension in dev to validate detection. + +Questions / decisions (resolved) +- preferredPackageManagerId: create a distinct `pipenv` package manager id: `ms-python.python:pipenv`. + +- Activation approach: use `pipenv shell` for terminal activation (interactive terminals) and `pipenv run` as the fallback for non-interactive runs / `activatedRun`. + +- Scope of discovery: discover both global pipenv virtualenvs and workspace-local pipenv environments (projects with Pipfile). + +- Create/quickCreate: implement `create()` using `pipenv install` to create environments and install requested packages as part of quick-create. + +- Windows/PowerShell specifics: keep `shellActivation` mapping with `'unknown'` fallback for now; revisit if issues surface. + +- Tests: (deferred). \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 22b2f658..159ee260 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -76,6 +76,7 @@ import { } from './managers/common/nativePythonFinder'; import { IDisposable } from './managers/common/types'; import { registerCondaFeatures } from './managers/conda/main'; +import { registerPipenvFeatures } from './managers/pipenv/main'; import { registerPoetryFeatures } from './managers/poetry/main'; import { registerPyenvFeatures } from './managers/pyenv/main'; @@ -562,6 +563,7 @@ export async function activate(context: ExtensionContext): Promise(); public readonly onDidChangeEnvironments = this._onDidChangeEnvironments.event; - constructor(private readonly nativeFinder: NativePythonFinder, private readonly api: PythonEnvironmentApi) { + + public readonly name: string; + public readonly displayName: string; + public readonly preferredPackageManagerId: string; + public readonly description?: string; + public readonly tooltip: string | MarkdownString; + public readonly iconPath?: IconPath; + + private _initialized: Deferred | undefined; + + constructor( + public readonly nativeFinder: NativePythonFinder, + public readonly api: PythonEnvironmentApi + ) { this.name = 'pipenv'; this.displayName = 'Pipenv'; - this.preferredPackageManagerId = 'ms-python.python:pip'; + this.preferredPackageManagerId = 'ms-python.python:pipenv'; this.tooltip = new MarkdownString(PipenvStrings.pipenvManager, true); } - name: string; - displayName: string; - preferredPackageManagerId: string; - description?: string; - tooltip: string | MarkdownString; - iconPath?: IconPath; - public dispose() { this.collection = []; this.fsPathToEnv.clear(); + this._onDidChangeEnvironment.dispose(); + this._onDidChangeEnvironments.dispose(); + } + + async initialize(): Promise { + if (this._initialized) { + return this._initialized.promise; + } + + this._initialized = createDeferred(); + + await withProgress( + { + location: ProgressLocation.Window, + title: PipenvStrings.pipenvDiscovering, + }, + async () => { + this.collection = await refreshPipenv(false, this.nativeFinder, this.api, this); + await this.loadEnvMap(); + + this._onDidChangeEnvironments.fire( + this.collection.map((e) => ({ environment: e, kind: EnvironmentChangeKind.add })), + ); + }, + ); + this._initialized.resolve(); + } + + private async loadEnvMap() { + // Load environment mappings for projects + const projects = this.api.getPythonProjects(); + for (const project of projects) { + const envPath = await getPipenvForWorkspace(project.uri.fsPath); + if (envPath) { + const env = this.findEnvironmentByPath(envPath); + if (env) { + this.fsPathToEnv.set(project.uri.fsPath, env); + } + } + } + + // Load global environment + const globalEnvPath = await getPipenvForGlobal(); + if (globalEnvPath) { + this.globalEnv = this.findEnvironmentByPath(globalEnvPath); + } + } + + private findEnvironmentByPath(fsPath: string): PythonEnvironment | undefined { + return this.collection.find((env) => + env.environmentPath.fsPath === fsPath || + env.execInfo?.run.executable === fsPath + ); } quickCreateConfig?(): QuickCreateConfig | undefined { @@ -64,30 +137,162 @@ export class PipenvManager implements EnvironmentManager { // To be implemented } - async refresh(_scope: RefreshEnvironmentsScope): Promise { - // To be implemented + async refresh(scope: RefreshEnvironmentsScope): Promise { + const hardRefresh = scope === undefined; // hard refresh when scope is undefined + + await withProgress( + { + location: ProgressLocation.Window, + title: PipenvStrings.pipenvRefreshing, + }, + async () => { + const oldCollection = [...this.collection]; + this.collection = await refreshPipenv(hardRefresh, this.nativeFinder, this.api, this); + await this.loadEnvMap(); + + // Fire change events for environments that were added or removed + const changes: { environment: PythonEnvironment; kind: EnvironmentChangeKind }[] = []; + + // Find removed environments + oldCollection.forEach((oldEnv) => { + if (!this.collection.find((newEnv) => newEnv.envId.id === oldEnv.envId.id)) { + changes.push({ environment: oldEnv, kind: EnvironmentChangeKind.remove }); + } + }); + + // Find added environments + this.collection.forEach((newEnv) => { + if (!oldCollection.find((oldEnv) => oldEnv.envId.id === newEnv.envId.id)) { + changes.push({ environment: newEnv, kind: EnvironmentChangeKind.add }); + } + }); + + if (changes.length > 0) { + this._onDidChangeEnvironments.fire(changes); + } + }, + ); } - async getEnvironments(_scope: GetEnvironmentsScope): Promise { - // To be implemented + async getEnvironments(scope: GetEnvironmentsScope): Promise { + await this.initialize(); + + if (scope === 'all') { + return Array.from(this.collection); + } + + if (scope === 'global') { + // Return all environments for global scope + return Array.from(this.collection); + } + + if (scope instanceof Uri) { + const project = this.api.getPythonProject(scope); + if (project) { + const env = this.fsPathToEnv.get(project.uri.fsPath); + return env ? [env] : []; + } + } + return []; } - async set(_scope: SetEnvironmentScope, _environment?: PythonEnvironment): Promise { - // To be implemented + async set(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise { + if (scope === undefined) { + // Global scope + const before = this.globalEnv; + this.globalEnv = environment; + await setPipenvForGlobal(environment?.environmentPath.fsPath); + + if (before?.envId.id !== this.globalEnv?.envId.id) { + this._onDidChangeEnvironment.fire({ uri: undefined, old: before, new: this.globalEnv }); + } + return; + } + + if (scope instanceof Uri) { + // Single project scope + const project = this.api.getPythonProject(scope); + if (!project) { + return; + } + + const before = this.fsPathToEnv.get(project.uri.fsPath); + if (environment) { + this.fsPathToEnv.set(project.uri.fsPath, environment); + } else { + this.fsPathToEnv.delete(project.uri.fsPath); + } + + await setPipenvForWorkspace(project.uri.fsPath, environment?.environmentPath.fsPath); + + if (before?.envId.id !== environment?.envId.id) { + this._onDidChangeEnvironment.fire({ uri: scope, old: before, new: environment }); + } + } + + if (Array.isArray(scope) && scope.every((u) => u instanceof Uri)) { + // Multiple projects scope + const projects: PythonProject[] = []; + scope + .map((s) => this.api.getPythonProject(s)) + .forEach((p) => { + if (p) { + projects.push(p); + } + }); + + const before: Map = new Map(); + projects.forEach((p) => { + before.set(p.uri.fsPath, this.fsPathToEnv.get(p.uri.fsPath)); + if (environment) { + this.fsPathToEnv.set(p.uri.fsPath, environment); + } else { + this.fsPathToEnv.delete(p.uri.fsPath); + } + }); + + await setPipenvForWorkspaces( + projects.map((p) => p.uri.fsPath), + environment?.environmentPath.fsPath, + ); + + projects.forEach((p) => { + const b = before.get(p.uri.fsPath); + if (b?.envId.id !== environment?.envId.id) { + this._onDidChangeEnvironment.fire({ uri: p.uri, old: b, new: environment }); + } + }); + } } - async get(_scope: GetEnvironmentScope): Promise { - // To be implemented + async get(scope: GetEnvironmentScope): Promise { + await this.initialize(); + + if (scope === undefined) { + return this.globalEnv; + } + + if (scope instanceof Uri) { + const project = this.api.getPythonProject(scope); + if (project) { + return this.fsPathToEnv.get(project.uri.fsPath); + } + } + return undefined; } - async resolve(_context: ResolveEnvironmentContext): Promise { - // To be implemented - return undefined; + async resolve(context: ResolveEnvironmentContext): Promise { + await this.initialize(); + return resolvePipenvPath(context.fsPath, this.nativeFinder, this.api, this); } async clearCache?(): Promise { - // To be implemented + await clearPipenvCache(); + this.collection = []; + this.fsPathToEnv.clear(); + this.globalEnv = undefined; + this._initialized = undefined; } } diff --git a/src/managers/pipenv/pipenvPackageManager.ts b/src/managers/pipenv/pipenvPackageManager.ts new file mode 100644 index 00000000..4c7e15f7 --- /dev/null +++ b/src/managers/pipenv/pipenvPackageManager.ts @@ -0,0 +1,73 @@ +import { EventEmitter, LogOutputChannel, MarkdownString } from 'vscode'; +import { + DidChangePackagesEventArgs, + IconPath, + Package, + PackageManager, + PackageManagementOptions, + PythonEnvironment, + PythonEnvironmentApi, +} from '../../api'; +import { traceInfo } from '../../common/logging'; + +export class PipenvPackageManager implements PackageManager { + public readonly name: string; + public readonly displayName?: string; + public readonly description?: string; + public readonly tooltip?: string | MarkdownString; + public readonly iconPath?: IconPath; + public readonly log?: LogOutputChannel; + + private readonly _onDidChangePackages = new EventEmitter(); + public readonly onDidChangePackages = this._onDidChangePackages.event; + + constructor( + public readonly api: PythonEnvironmentApi, + log?: LogOutputChannel + ) { + this.name = 'pipenv'; + this.displayName = 'Pipenv'; + this.description = 'Manages packages using Pipenv'; + this.tooltip = new MarkdownString('Install and manage packages using Pipenv package manager'); + this.log = log; + } + + async manage(environment: PythonEnvironment, options: PackageManagementOptions): Promise { + // TODO: Implement pipenv package management + // This would run commands like: + // - pipenv install for installation + // - pipenv uninstall for uninstallation + // - pipenv install for installing from Pipfile + + traceInfo(`Pipenv package management not yet implemented for environment: ${environment.name}`); + traceInfo(`Options: ${JSON.stringify(options)}`); + + // For now, just log the operation + if (options.install && options.install.length > 0) { + traceInfo(`Would install packages: ${options.install.join(', ')}`); + } + if (options.uninstall && options.uninstall.length > 0) { + traceInfo(`Would uninstall packages: ${options.uninstall.join(', ')}`); + } + + // Fire change event (though packages haven't actually changed) + // this._onDidChangePackages.fire({ changes: [] }); + } + + async refresh(environment: PythonEnvironment): Promise { + // TODO: Implement package list refresh + // This would run 'pipenv graph' or similar to get package list + traceInfo(`Pipenv package refresh not yet implemented for environment: ${environment.name}`); + } + + async getPackages(environment: PythonEnvironment): Promise { + // TODO: Implement package listing + // This would parse output from 'pipenv graph' or 'pip list' in the pipenv environment + traceInfo(`Pipenv package listing not yet implemented for environment: ${environment.name}`); + return []; + } + + public dispose() { + this._onDidChangePackages.dispose(); + } +} \ No newline at end of file diff --git a/src/managers/pipenv/pipenvUtils.ts b/src/managers/pipenv/pipenvUtils.ts index 48997450..2a16cf6d 100644 --- a/src/managers/pipenv/pipenvUtils.ts +++ b/src/managers/pipenv/pipenvUtils.ts @@ -1,11 +1,246 @@ // Utility functions for Pipenv environment management -import { NativePythonFinder } from '../common/nativePythonFinder'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import which from 'which'; +import { + EnvironmentManager, + PythonCommandRunConfiguration, + PythonEnvironment, + PythonEnvironmentApi, + PythonEnvironmentInfo, +} from '../../api'; +import { ENVS_EXTENSION_ID } from '../../common/constants'; +import { traceError, traceInfo } from '../../common/logging'; +import { getWorkspacePersistentState } from '../../common/persistentState'; +import { + isNativeEnvInfo, + NativeEnvInfo, + NativeEnvManagerInfo, + NativePythonEnvironmentKind, + NativePythonFinder, +} from '../common/nativePythonFinder'; +import { getShellActivationCommands, shortVersion } from '../common/utils'; -export class PipenvUtils { - // Add static helper methods for pipenv operations here +export const PIPENV_PATH_KEY = `${ENVS_EXTENSION_ID}:pipenv:PIPENV_PATH`; +export const PIPENV_WORKSPACE_KEY = `${ENVS_EXTENSION_ID}:pipenv:WORKSPACE_SELECTED`; +export const PIPENV_GLOBAL_KEY = `${ENVS_EXTENSION_ID}:pipenv:GLOBAL_SELECTED`; + +let pipenvPath: string | undefined; + +async function findPipenv(): Promise { + try { + return await which('pipenv'); + } catch { + return undefined; + } } -export async function getPipenv(_native?: NativePythonFinder): Promise { - // Implementation to find and return the pipenv path + +async function setPipenv(pipenv: string): Promise { + pipenvPath = pipenv; + const state = await getWorkspacePersistentState(); + await state.set(PIPENV_PATH_KEY, pipenv); +} + +export async function clearPipenvCache(): Promise { + pipenvPath = undefined; +} + +export async function getPipenv(native?: NativePythonFinder): Promise { + if (pipenvPath) { + return pipenvPath; + } + + const state = await getWorkspacePersistentState(); + pipenvPath = await state.get(PIPENV_PATH_KEY); + if (pipenvPath) { + traceInfo(`Using pipenv from persistent state: ${pipenvPath}`); + return pipenvPath; + } + + // Try to find pipenv in PATH + const foundPipenv = await findPipenv(); + if (foundPipenv) { + pipenvPath = foundPipenv; + traceInfo(`Found pipenv in PATH: ${foundPipenv}`); + return foundPipenv; + } + + // Use native finder as fallback + if (native) { + const data = await native.refresh(false); + const managers = data + .filter((e) => !isNativeEnvInfo(e)) + .map((e) => e as NativeEnvManagerInfo) + .filter((e) => e.tool.toLowerCase() === 'pipenv'); + if (managers.length > 0) { + pipenvPath = managers[0].executable; + traceInfo(`Using pipenv from native finder: ${pipenvPath}`); + await state.set(PIPENV_PATH_KEY, pipenvPath); + return pipenvPath; + } + } + + traceInfo('Pipenv not found'); return undefined; } + +async function nativeToPythonEnv( + info: NativeEnvInfo, + api: PythonEnvironmentApi, + manager: EnvironmentManager, +): Promise { + if (!(info.prefix && info.executable && info.version)) { + traceError(`Incomplete pipenv environment info: ${JSON.stringify(info)}`); + return undefined; + } + + const sv = shortVersion(info.version); + const name = info.name || info.displayName || path.basename(info.prefix); + const displayName = info.displayName || `pipenv (${sv})`; + + // Derive the environment's bin/scripts directory from the python executable + const binDir = path.dirname(info.executable); + let shellActivation: Map = new Map(); + let shellDeactivation: Map = new Map(); + + try { + const maps = await getShellActivationCommands(binDir); + shellActivation = maps.shellActivation; + shellDeactivation = maps.shellDeactivation; + } catch (ex) { + traceError(`Failed to compute shell activation commands for pipenv at ${binDir}: ${ex}`); + } + + const environment: PythonEnvironmentInfo = { + name: name, + displayName: displayName, + shortDisplayName: displayName, + displayPath: info.prefix, + version: info.version, + environmentPath: Uri.file(info.prefix), + description: undefined, + tooltip: info.prefix, + execInfo: { + run: { executable: info.executable }, + shellActivation, + shellDeactivation, + }, + sysPrefix: info.prefix, + }; + + return api.createPythonEnvironmentItem(environment, manager); +} + +export async function refreshPipenv( + hardRefresh: boolean, + nativeFinder: NativePythonFinder, + api: PythonEnvironmentApi, + manager: EnvironmentManager, +): Promise { + traceInfo('Refreshing pipenv environments'); + const data = await nativeFinder.refresh(hardRefresh); + + let pipenv = await getPipenv(); + + if (pipenv === undefined) { + const managers = data + .filter((e) => !isNativeEnvInfo(e)) + .map((e) => e as NativeEnvManagerInfo) + .filter((e) => e.tool.toLowerCase() === 'pipenv'); + + if (managers.length > 0) { + pipenv = managers[0].executable; + await setPipenv(pipenv); + } + } + + const envs = data + .filter((e) => isNativeEnvInfo(e)) + .map((e) => e as NativeEnvInfo) + .filter((e) => e.kind === NativePythonEnvironmentKind.pipenv); + + const collection: PythonEnvironment[] = []; + + for (const e of envs) { + if (pipenv) { + const environment = await nativeToPythonEnv(e, api, manager); + if (environment) { + collection.push(environment); + } + } + } + + traceInfo(`Found ${collection.length} pipenv environments`); + return collection; +} + +export async function resolvePipenvPath( + fsPath: string, + nativeFinder: NativePythonFinder, + api: PythonEnvironmentApi, + manager: EnvironmentManager, +): Promise { + const resolved = await nativeFinder.resolve(fsPath); + + if (resolved.kind === NativePythonEnvironmentKind.pipenv) { + const pipenv = await getPipenv(nativeFinder); + if (pipenv) { + return await nativeToPythonEnv(resolved, api, manager); + } + } + + return undefined; +} + +// Persistence functions for workspace/global environment selection +export async function getPipenvForGlobal(): Promise { + const state = await getWorkspacePersistentState(); + return await state.get(PIPENV_GLOBAL_KEY); +} + +export async function setPipenvForGlobal(pipenvPath: string | undefined): Promise { + const state = await getWorkspacePersistentState(); + await state.set(PIPENV_GLOBAL_KEY, pipenvPath); +} + +export async function getPipenvForWorkspace(fsPath: string): Promise { + const state = await getWorkspacePersistentState(); + const data: { [key: string]: string } | undefined = await state.get(PIPENV_WORKSPACE_KEY); + if (data) { + try { + return data[fsPath]; + } catch { + return undefined; + } + } + return undefined; +} + +export async function setPipenvForWorkspace(fsPath: string, envPath: string | undefined): Promise { + const state = await getWorkspacePersistentState(); + const data: { [key: string]: string } = (await state.get(PIPENV_WORKSPACE_KEY)) ?? {}; + if (envPath) { + data[fsPath] = envPath; + } else { + delete data[fsPath]; + } + await state.set(PIPENV_WORKSPACE_KEY, data); +} + +export async function setPipenvForWorkspaces(fsPath: string[], envPath: string | undefined): Promise { + const state = await getWorkspacePersistentState(); + const data: { [key: string]: string } = (await state.get(PIPENV_WORKSPACE_KEY)) ?? {}; + fsPath.forEach((s) => { + if (envPath) { + data[s] = envPath; + } else { + delete data[s]; + } + }); + await state.set(PIPENV_WORKSPACE_KEY, data); +} + +export class PipenvUtils { + // Add static helper methods for pipenv operations here +} From 6136127182914f715ca2751d7fb15f5068636663 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 21 Aug 2025 16:23:47 -0700 Subject: [PATCH 3/5] use pip as default manager --- src/managers/pipenv/main.ts | 11 +--- src/managers/pipenv/pipenvManager.ts | 20 +++--- src/managers/pipenv/pipenvPackageManager.ts | 73 --------------------- 3 files changed, 10 insertions(+), 94 deletions(-) delete mode 100644 src/managers/pipenv/pipenvPackageManager.ts diff --git a/src/managers/pipenv/main.ts b/src/managers/pipenv/main.ts index 7f1e4b70..35e907f2 100644 --- a/src/managers/pipenv/main.ts +++ b/src/managers/pipenv/main.ts @@ -4,7 +4,6 @@ import { traceInfo } from '../../common/logging'; import { getPythonApi } from '../../features/pythonApi'; import { NativePythonFinder } from '../common/nativePythonFinder'; import { PipenvManager } from './pipenvManager'; -import { PipenvPackageManager } from './pipenvPackageManager'; import { getPipenv } from './pipenvUtils'; export async function registerPipenvFeatures( @@ -18,14 +17,8 @@ export async function registerPipenvFeatures( if (pipenv) { const mgr = new PipenvManager(nativeFinder, api); - const packageManager = new PipenvPackageManager(api); - - disposables.push( - mgr, - packageManager, - api.registerEnvironmentManager(mgr), - api.registerPackageManager(packageManager) - ); + + disposables.push(mgr, api.registerEnvironmentManager(mgr)); } else { traceInfo('Pipenv not found, turning off pipenv features.'); } diff --git a/src/managers/pipenv/pipenvManager.ts b/src/managers/pipenv/pipenvManager.ts index 54e74572..6f637b56 100644 --- a/src/managers/pipenv/pipenvManager.ts +++ b/src/managers/pipenv/pipenvManager.ts @@ -52,13 +52,10 @@ export class PipenvManager implements EnvironmentManager { private _initialized: Deferred | undefined; - constructor( - public readonly nativeFinder: NativePythonFinder, - public readonly api: PythonEnvironmentApi - ) { + constructor(public readonly nativeFinder: NativePythonFinder, public readonly api: PythonEnvironmentApi) { this.name = 'pipenv'; this.displayName = 'Pipenv'; - this.preferredPackageManagerId = 'ms-python.python:pipenv'; + this.preferredPackageManagerId = 'ms-python.python:pip'; this.tooltip = new MarkdownString(PipenvStrings.pipenvManager, true); } @@ -114,9 +111,8 @@ export class PipenvManager implements EnvironmentManager { } private findEnvironmentByPath(fsPath: string): PythonEnvironment | undefined { - return this.collection.find((env) => - env.environmentPath.fsPath === fsPath || - env.execInfo?.run.executable === fsPath + return this.collection.find( + (env) => env.environmentPath.fsPath === fsPath || env.execInfo?.run.executable === fsPath, ); } @@ -139,7 +135,7 @@ export class PipenvManager implements EnvironmentManager { async refresh(scope: RefreshEnvironmentsScope): Promise { const hardRefresh = scope === undefined; // hard refresh when scope is undefined - + await withProgress( { location: ProgressLocation.Window, @@ -152,7 +148,7 @@ export class PipenvManager implements EnvironmentManager { // Fire change events for environments that were added or removed const changes: { environment: PythonEnvironment; kind: EnvironmentChangeKind }[] = []; - + // Find removed environments oldCollection.forEach((oldEnv) => { if (!this.collection.find((newEnv) => newEnv.envId.id === oldEnv.envId.id)) { @@ -203,7 +199,7 @@ export class PipenvManager implements EnvironmentManager { const before = this.globalEnv; this.globalEnv = environment; await setPipenvForGlobal(environment?.environmentPath.fsPath); - + if (before?.envId.id !== this.globalEnv?.envId.id) { this._onDidChangeEnvironment.fire({ uri: undefined, old: before, new: this.globalEnv }); } @@ -223,7 +219,7 @@ export class PipenvManager implements EnvironmentManager { } else { this.fsPathToEnv.delete(project.uri.fsPath); } - + await setPipenvForWorkspace(project.uri.fsPath, environment?.environmentPath.fsPath); if (before?.envId.id !== environment?.envId.id) { diff --git a/src/managers/pipenv/pipenvPackageManager.ts b/src/managers/pipenv/pipenvPackageManager.ts deleted file mode 100644 index 4c7e15f7..00000000 --- a/src/managers/pipenv/pipenvPackageManager.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { EventEmitter, LogOutputChannel, MarkdownString } from 'vscode'; -import { - DidChangePackagesEventArgs, - IconPath, - Package, - PackageManager, - PackageManagementOptions, - PythonEnvironment, - PythonEnvironmentApi, -} from '../../api'; -import { traceInfo } from '../../common/logging'; - -export class PipenvPackageManager implements PackageManager { - public readonly name: string; - public readonly displayName?: string; - public readonly description?: string; - public readonly tooltip?: string | MarkdownString; - public readonly iconPath?: IconPath; - public readonly log?: LogOutputChannel; - - private readonly _onDidChangePackages = new EventEmitter(); - public readonly onDidChangePackages = this._onDidChangePackages.event; - - constructor( - public readonly api: PythonEnvironmentApi, - log?: LogOutputChannel - ) { - this.name = 'pipenv'; - this.displayName = 'Pipenv'; - this.description = 'Manages packages using Pipenv'; - this.tooltip = new MarkdownString('Install and manage packages using Pipenv package manager'); - this.log = log; - } - - async manage(environment: PythonEnvironment, options: PackageManagementOptions): Promise { - // TODO: Implement pipenv package management - // This would run commands like: - // - pipenv install for installation - // - pipenv uninstall for uninstallation - // - pipenv install for installing from Pipfile - - traceInfo(`Pipenv package management not yet implemented for environment: ${environment.name}`); - traceInfo(`Options: ${JSON.stringify(options)}`); - - // For now, just log the operation - if (options.install && options.install.length > 0) { - traceInfo(`Would install packages: ${options.install.join(', ')}`); - } - if (options.uninstall && options.uninstall.length > 0) { - traceInfo(`Would uninstall packages: ${options.uninstall.join(', ')}`); - } - - // Fire change event (though packages haven't actually changed) - // this._onDidChangePackages.fire({ changes: [] }); - } - - async refresh(environment: PythonEnvironment): Promise { - // TODO: Implement package list refresh - // This would run 'pipenv graph' or similar to get package list - traceInfo(`Pipenv package refresh not yet implemented for environment: ${environment.name}`); - } - - async getPackages(environment: PythonEnvironment): Promise { - // TODO: Implement package listing - // This would parse output from 'pipenv graph' or 'pip list' in the pipenv environment - traceInfo(`Pipenv package listing not yet implemented for environment: ${environment.name}`); - return []; - } - - public dispose() { - this._onDidChangePackages.dispose(); - } -} \ No newline at end of file From 5f92aadcae2d9ce20fea2b4178500e8b9a5773be Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 21 Aug 2025 16:27:23 -0700 Subject: [PATCH 4/5] remove doc --- docs/pipenv-integration.md | 74 -------------------------------------- 1 file changed, 74 deletions(-) delete mode 100644 docs/pipenv-integration.md diff --git a/docs/pipenv-integration.md b/docs/pipenv-integration.md deleted file mode 100644 index 0d6a5bab..00000000 --- a/docs/pipenv-integration.md +++ /dev/null @@ -1,74 +0,0 @@ -# Pipenv Environment Manager — Implementation Plan - -Summary -- This doc lists the tasks required to get pipenv integrated as an EnvironmentManager - -Files & methods to implement -1. Registration/activation (src/managers/pipenv/main.ts) - - Implement `registerPipenvFeatures(nativeFinder: NativePythonFinder, disposables: Disposable[])`. - - Use the same registration pattern as `pyenv` and `poetry` (get Python API, detect pipenv, instantiate manager, `api.registerEnvironmentManager(mgr)`, push disposables). - -2. Utilities (src/managers/pipenv/pipenvUtils.ts) - - `getPipenv(native?: NativePythonFinder)` — locate the `pipenv` binary (persisted override, env vars, which, or `nativeFinder` fallback). - - `refreshPipenv(hardRefresh, nativeFinder, api, manager)` — discover pipenv environments (workspace Pipfiles, native finder info, or scanning `WORKON_HOME`). - - `resolvePipenvPath(fsPath, nativeFinder, api, manager)` — resolve a path or Uri to a PythonEnvironment. - - `nativeToPythonEnv(nativeInfo, api, manager, pipenvPath)` — convert native discovery info into a `PythonEnvironment`: - - set `execInfo.run.executable` (use `pipenv --py` when possible) and `execInfo.shellActivation` (see activation strategy below), `sysPrefix`, display metadata. - -3. Manager implementation (src/managers/pipenv/pipenvManager.ts) - - Implement class like `PoetryManager`/`PyEnvManager`: - - fields: `collection: PythonEnvironment[]`, `fsPathToEnv: Map`, `globalEnv`. - - event emitters: `_onDidChangeEnvironments`, `_onDidChangeEnvironment` and public events. - - constructor(nativeFinder, api) and metadata properties (`name`, `displayName`, `preferredPackageManagerId`, `tooltip`). - - lifecycle methods: `initialize()`, `getEnvironments()`, `refresh()`, `get()`, `set()`, `resolve()`, `clearCache()`. - - helpers: `loadEnvMap()`, `fromEnvMap(uri)`, `findEnvironmentByPath(fsPath)`. - - Use `api.createPythonEnvironmentItem()` to create `PythonEnvironment` items. - -4. Exec info & activation behavior - - Resolve a Python executable using `pipenv --py` when possible and set `execInfo.run.executable`. - - Activation options: - - Provide `shellActivation` mapping with `'unknown'` fallback. For example `{ executable: 'pipenv', args: ['shell'] }` for activation. - - Provide `activatedRun` or `run` that uses resolved python (`/path/to/venv/bin/python`) or fallback to `pipenv run python` (e.g., run.executable = 'pipenv', args = ['run', 'python']). - - Set `shellDeactivation` to `exit`/`deactivate` where appropriate. - -5. Workspace mapping & persistence - - Implement per-workspace persistent selection (get/set persisted environment for workspace & global), similar to `pyenv` and `poetry` utils. - - Implement logic in `loadEnvMap()` to pick project-specific envs (Pipfile location), global fallback, and mapping to projects via `api.getPythonProjects()`. - -6. Package-manager (required) - - Implement a dedicated Pipenv `PackageManager` and register it via `api.registerPackageManager(...)`. - - Use package manager id: `ms-python.python:pipenv`. - - Implement install/uninstall by invoking `pipenv install`/`pipenv uninstall` and firing package-change events. - -7. Tests - - Add unit tests (mocking `NativePythonFinder` and `getPythonApi`) for detection, discovery, `resolve()` and mapping. - - Add integration tests that run `pipenv --py`/`pipenv --venv` behavior using a test fixture if desired. - -8. Localization & assets - - Add localized strings (e.g., `PipenvStrings`) for messages and progress titles. - - Add icon(s) if required and reference via `iconPath`. - -9. Documentation - - Update README/docs to include Pipenv support and configuration/setting notes. - -10. CI & linting - - Run tests and fix TypeScript compile/lint issues (unused args, correct imports). Ensure `main.ts` registration uses `api.registerEnvironmentManager` like other managers. - -Minimal viable implementation (priority) -1. Fix `main.ts` to implement `registerPipenvFeatures(...)` and register the manager (so the manager is known to the extension). -2. Implement `getPipenv()` (detect pipenv binary) and `nativeToPythonEnv()` (at minimum obtain python path using `pipenv --py` and return a valid `PythonEnvironment` via `api.createPythonEnvironmentItem`). -3. Implement manager skeleton (constructor, event emitters, `initialize()`, `getEnvironments()` and `resolve()` that uses utils above) and wire registration. -4. Add a simple integration test and run the extension in dev to validate detection. - -Questions / decisions (resolved) -- preferredPackageManagerId: create a distinct `pipenv` package manager id: `ms-python.python:pipenv`. - -- Activation approach: use `pipenv shell` for terminal activation (interactive terminals) and `pipenv run` as the fallback for non-interactive runs / `activatedRun`. - -- Scope of discovery: discover both global pipenv virtualenvs and workspace-local pipenv environments (projects with Pipfile). - -- Create/quickCreate: implement `create()` using `pipenv install` to create environments and install requested packages as part of quick-create. - -- Windows/PowerShell specifics: keep `shellActivation` mapping with `'unknown'` fallback for now; revisit if issues surface. - -- Tests: (deferred). \ No newline at end of file From 4c42aa8aba77aad2a507e1b09708d32ada45b810 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 21 Aug 2025 16:32:23 -0700 Subject: [PATCH 5/5] remove unused code --- src/managers/pipenv/pipenvManager.ts | 10 ---------- src/managers/pipenv/pipenvUtils.ts | 4 ---- 2 files changed, 14 deletions(-) diff --git a/src/managers/pipenv/pipenvManager.ts b/src/managers/pipenv/pipenvManager.ts index 6f637b56..951a8dcd 100644 --- a/src/managers/pipenv/pipenvManager.ts +++ b/src/managers/pipenv/pipenvManager.ts @@ -12,7 +12,6 @@ import { PythonEnvironment, PythonEnvironmentApi, PythonProject, - QuickCreateConfig, RefreshEnvironmentsScope, ResolveEnvironmentContext, SetEnvironmentScope, @@ -116,11 +115,6 @@ export class PipenvManager implements EnvironmentManager { ); } - quickCreateConfig?(): QuickCreateConfig | undefined { - // To be implemented - return undefined; - } - async create?( _scope: CreateEnvironmentScope, _options?: CreateEnvironmentOptions, @@ -129,10 +123,6 @@ export class PipenvManager implements EnvironmentManager { return undefined; } - async remove?(_environment: PythonEnvironment): Promise { - // To be implemented - } - async refresh(scope: RefreshEnvironmentsScope): Promise { const hardRefresh = scope === undefined; // hard refresh when scope is undefined diff --git a/src/managers/pipenv/pipenvUtils.ts b/src/managers/pipenv/pipenvUtils.ts index 2a16cf6d..3c271d44 100644 --- a/src/managers/pipenv/pipenvUtils.ts +++ b/src/managers/pipenv/pipenvUtils.ts @@ -240,7 +240,3 @@ export async function setPipenvForWorkspaces(fsPath: string[], envPath: string | }); await state.set(PIPENV_WORKSPACE_KEY, data); } - -export class PipenvUtils { - // Add static helper methods for pipenv operations here -}