Skip to content
Open
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
14 changes: 10 additions & 4 deletions src/managers/builtin/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,18 @@ export async function refreshPythons(

const PIP_LIST_TIMEOUT_MS = 30_000;

function getUvEnvironmentTarget(environment: PythonEnvironment): string {
// A virtual environment's executable may resolve to its base interpreter. uv accepts
// the environment directory directly, which preserves the environment boundary.
return environment.environmentPath.fsPath;
}

async function execPipList(environment: PythonEnvironment, log?: LogOutputChannel, args?: string[]): Promise<string> {
// Use environmentPath directly for consistency with UV environment tracking
const useUv = await shouldUseUv(log, environment.environmentPath.fsPath);
if (useUv) {
return await runUV(
['pip', 'list', '--python', environment.execInfo.run.executable, '--format=json', ...(args ?? [])],
['pip', 'list', '--python', getUvEnvironmentTarget(environment), '--format=json', ...(args ?? [])],
undefined,
log,
undefined,
Expand Down Expand Up @@ -258,7 +264,7 @@ export async function refreshPipDirectPackageNames(
const useUv = await shouldUseUv(log, environment.environmentPath.fsPath);
if (useUv) {
const treeOutput = await runUV(
['pip', 'tree', '--python', environment.execInfo.run.executable, '--depth=0'],
['pip', 'tree', '--python', getUvEnvironmentTarget(environment), '--depth=0'],
undefined,
log,
undefined,
Expand Down Expand Up @@ -287,7 +293,7 @@ export async function managePackages(
if (options.uninstall && options.uninstall.length > 0) {
if (useUv) {
await runUV(
[...uninstallArgs, '--python', environment.execInfo.run.executable, ...options.uninstall],
[...uninstallArgs, '--python', getUvEnvironmentTarget(environment), ...options.uninstall],
undefined,
manager.log,
token,
Expand All @@ -313,7 +319,7 @@ export async function managePackages(

if (useUv) {
await runUV(
[...installArgs, '--python', environment.execInfo.run.executable, ...processedInstallArgs],
[...installArgs, '--python', getUvEnvironmentTarget(environment), ...processedInstallArgs],
undefined,
manager.log,
token,
Expand Down
101 changes: 101 additions & 0 deletions src/test/managers/builtin/packageManagement.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import assert from 'assert';
import * as path from 'path';
import * as sinon from 'sinon';
import * as typeMoq from 'typemoq';
import { LogOutputChannel, Uri } from 'vscode';
import { PackageManager, PythonEnvironment } from '../../../api';
import * as helpers from '../../../managers/builtin/helpers';
import {
managePackages,
refreshPipDirectPackageNames,
refreshPipPackages,
} from '../../../managers/builtin/utils';
import { createMockLogOutputChannel, setupNonThenable } from '../../mocks/helper';

suite('Pip package management', () => {
let environment: typeMoq.IMock<PythonEnvironment>;
let manager: typeMoq.IMock<PackageManager>;
let log: LogOutputChannel;
let runUvStub: sinon.SinonStub;
let environmentTarget: string;
let baseInterpreter: string;

setup(() => {
environmentTarget = Uri.file(path.join('test-data', 'pipenv-environment')).fsPath;
baseInterpreter = Uri.file(path.join('test-data', 'uv', 'python')).fsPath;

environment = typeMoq.Mock.ofType<PythonEnvironment>();
environment.setup((env) => env.version).returns(() => '3.13.0');
environment.setup((env) => env.environmentPath).returns(() => Uri.file(environmentTarget));
environment.setup((env) => env.execInfo).returns(() => ({ run: { executable: baseInterpreter } }));
setupNonThenable(environment);

log = createMockLogOutputChannel();
manager = typeMoq.Mock.ofType<PackageManager>();
manager.setup((packageManager) => packageManager.log).returns(() => log);
setupNonThenable(manager);

sinon.stub(helpers, 'shouldUseUv').resolves(true);
runUvStub = sinon.stub(helpers, 'runUV').resolves('');
});

teardown(() => {
sinon.restore();
});

test('uses the environment path when listing packages with uv', async () => {
runUvStub.resolves('[]');

const packages = await refreshPipPackages(environment.object, log);

assert.deepStrictEqual(packages, []);
sinon.assert.calledWithExactly(
runUvStub,
['pip', 'list', '--python', environmentTarget, '--format=json'],
undefined,
log,
undefined,
30_000,
);
assert.ok(!runUvStub.calledWith(sinon.match.array.contains([baseInterpreter])));
});

test('uses the environment path when finding direct packages with uv', async () => {
await refreshPipDirectPackageNames(environment.object, log);

sinon.assert.calledWithExactly(
runUvStub,
['pip', 'tree', '--python', environmentTarget, '--depth=0'],
undefined,
log,
undefined,
30_000,
);
assert.ok(!runUvStub.calledWith(sinon.match.array.contains([baseInterpreter])));
});

test('uses the environment path when installing and uninstalling packages with uv', async () => {
await managePackages(
environment.object,
{ install: ['absl-py'], uninstall: ['setuptools'] },
manager.object,
);

sinon.assert.calledTwice(runUvStub);
sinon.assert.calledWithExactly(
runUvStub.firstCall,
['pip', 'uninstall', '--python', environmentTarget, 'setuptools'],
undefined,
log,
undefined,
);
sinon.assert.calledWithExactly(
runUvStub.secondCall,
['pip', 'install', '--python', environmentTarget, 'absl-py'],
undefined,
log,
undefined,
);
assert.ok(!runUvStub.calledWith(sinon.match.array.contains([baseInterpreter])));
});
});
Loading