Skip to content

Commit 349b34a

Browse files
Copilotedvilme
andauthored
Target environment directory for uv package commands
Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com>
1 parent ca80254 commit 349b34a

2 files changed

Lines changed: 95 additions & 7 deletions

File tree

src/managers/builtin/commands/factory.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@ export async function createPipOrUvCommandWithKind<P, U>(
1111
PipCommand: CommandConstructor<P>,
1212
UvCommand: CommandConstructor<U>,
1313
): Promise<PipOrUvCommand<P, U>> {
14-
return (await shouldUseUv(options.log, environmentPath))
15-
? { kind: 'uv', command: new UvCommand(options) }
16-
: { kind: 'pip', command: new PipCommand(options) };
14+
if (await shouldUseUv(options.log, environmentPath)) {
15+
// uv accepts an environment directory as its `--python` target. A symlinked
16+
// environment executable (for example Pipenv) can resolve to the externally
17+
// managed base interpreter, so passing the environment directory preserves
18+
// the environment boundary. Pip commands keep using the interpreter itself.
19+
return { kind: 'uv', command: new UvCommand({ ...options, pythonExecutable: environmentPath }) };
20+
}
21+
return { kind: 'pip', command: new PipCommand(options) };
1722
}
1823

1924
export async function createPipOrUvCommand<T, P extends T, U extends T>(

src/test/managers/builtin/commands.unit.test.ts

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@ import {
1313
PipAvailableVersionsTextCommand,
1414
UvAvailableVersionsCommand,
1515
} from '../../../managers/builtin/commands/availableVersions';
16+
import { createPipOrUvCommand } from '../../../managers/builtin/commands/factory';
1617
import { PipInstallCommand, UvInstallCommand } from '../../../managers/builtin/commands/install';
1718
import { PipListCommand, UvListCommand } from '../../../managers/builtin/commands/list';
18-
import { PipListDirectNamesCommand, UvListDirectNamesCommand } from '../../../managers/builtin/commands/listDirectNames';
19+
import {
20+
PipListDirectNamesCommand,
21+
UvListDirectNamesCommand,
22+
} from '../../../managers/builtin/commands/listDirectNames';
1923
import { PipUninstallCommand, UvUninstallCommand } from '../../../managers/builtin/commands/uninstall';
2024
import { PipVersionCommand, UvVersionCommand } from '../../../managers/builtin/commands/version';
2125
import * as helpers from '../../../managers/builtin/helpers';
@@ -28,6 +32,7 @@ suite('Pip and UV command parsing', () => {
2832
let mockLog: LogOutputChannel;
2933
let runPythonStub: sinon.SinonStub;
3034
let runUvStub: sinon.SinonStub;
35+
let shouldUseUvStub: sinon.SinonStub;
3136

3237
setup(() => {
3338
log = createMockLogOutputChannel();
@@ -37,6 +42,7 @@ suite('Pip and UV command parsing', () => {
3742
} as unknown as ReturnType<typeof workspaceApis.getConfiguration>);
3843
runPythonStub = sinon.stub(helpers, 'runPython').resolves('');
3944
runUvStub = sinon.stub(helpers, 'runUV').resolves('');
45+
shouldUseUvStub = sinon.stub(helpers, 'shouldUseUv').resolves(false);
4046
});
4147

4248
teardown(() => {
@@ -259,7 +265,10 @@ suite('Pip and UV command parsing', () => {
259265
'--python-version',
260266
'3.13.1',
261267
]);
262-
assert.deepStrictEqual(result.map((version) => version.public), ['1.0.0']);
268+
assert.deepStrictEqual(
269+
result.map((version) => version.public),
270+
['1.0.0'],
271+
);
263272
});
264273

265274
test('PipAvailableVersionsTextCommand parses text output for Pip 21.2 through 25.0', async () => {
@@ -282,7 +291,10 @@ suite('Pip and UV command parsing', () => {
282291
'--python-version',
283292
'3.13.1',
284293
]);
285-
assert.deepStrictEqual(result.map((version) => version.public), ['1.0.0']);
294+
assert.deepStrictEqual(
295+
result.map((version) => version.public),
296+
['1.0.0'],
297+
);
286298
});
287299

288300
test('UvAvailableVersionsCommand rejects output surrounding JSON', async () => {
@@ -319,7 +331,10 @@ suite('Pip and UV command parsing', () => {
319331
const result = await command.execute();
320332

321333
assert.deepStrictEqual(runUvStub.firstCall.args[0], ['pip', 'list', '--format=json', '--python', 'python']);
322-
assert.deepStrictEqual(result.map((pkg) => pkg.name), ['package']);
334+
assert.deepStrictEqual(
335+
result.map((pkg) => pkg.name),
336+
['package'],
337+
);
323338
});
324339

325340
test('direct-package commands normalize names and ignore UV dependencies', async () => {
@@ -349,4 +364,72 @@ suite('Pip and UV command parsing', () => {
349364
assert.strictEqual(pipVersion?.public, '24.0');
350365
assert.strictEqual(uvVersion?.public, '0.4.20');
351366
});
367+
368+
test('UV package commands target the environment directory rather than the resolved interpreter', async () => {
369+
const baseInterpreter = path.join(path.sep, 'uv', 'python', 'cpython-3.13', 'bin', 'python');
370+
const environmentPath = path.join(path.sep, 'virtualenvs', 'pipenv-project');
371+
shouldUseUvStub.resolves(true);
372+
const options = { pythonExecutable: baseInterpreter, log: mockLog };
373+
374+
const install: PipInstallCommand | UvInstallCommand = await createPipOrUvCommand(
375+
options,
376+
environmentPath,
377+
PipInstallCommand,
378+
UvInstallCommand,
379+
);
380+
await install.execute({ packages: [{ packageName: 'requests' }] });
381+
382+
const uninstall: PipUninstallCommand | UvUninstallCommand = await createPipOrUvCommand(
383+
options,
384+
environmentPath,
385+
PipUninstallCommand,
386+
UvUninstallCommand,
387+
);
388+
await uninstall.execute({ packages: [{ packageName: 'requests' }] });
389+
390+
const list: PipListCommand | UvListCommand = await createPipOrUvCommand(
391+
options,
392+
environmentPath,
393+
PipListCommand,
394+
UvListCommand,
395+
);
396+
runUvStub.resolves('[]');
397+
await list.execute();
398+
399+
const directNames: PipListDirectNamesCommand | UvListDirectNamesCommand = await createPipOrUvCommand(
400+
options,
401+
environmentPath,
402+
PipListDirectNamesCommand,
403+
UvListDirectNamesCommand,
404+
);
405+
runUvStub.resolves('');
406+
await directNames.execute();
407+
408+
assert.strictEqual(runPythonStub.callCount, 0);
409+
assert.strictEqual(runUvStub.callCount, 4);
410+
for (const call of runUvStub.getCalls()) {
411+
const args = call.args[0] as string[];
412+
const pythonIndex = args.indexOf('--python');
413+
assert.notStrictEqual(pythonIndex, -1);
414+
assert.strictEqual(args[pythonIndex + 1], environmentPath);
415+
assert.ok(!args.includes(baseInterpreter));
416+
}
417+
});
418+
419+
test('Pip package commands continue using the environment interpreter', async () => {
420+
const pythonExecutable = path.join(path.sep, 'virtualenvs', 'project', 'bin', 'python');
421+
const environmentPath = path.join(path.sep, 'virtualenvs', 'project');
422+
shouldUseUvStub.resolves(false);
423+
424+
const install: PipInstallCommand | UvInstallCommand = await createPipOrUvCommand(
425+
{ pythonExecutable, log: mockLog },
426+
environmentPath,
427+
PipInstallCommand,
428+
UvInstallCommand,
429+
);
430+
await install.execute({ packages: [{ packageName: 'requests' }] });
431+
432+
assert.strictEqual(runPythonStub.firstCall.args[0], pythonExecutable);
433+
assert.ok(runUvStub.notCalled);
434+
});
352435
});

0 commit comments

Comments
 (0)