Skip to content

Commit c41df06

Browse files
authored
Use UV installer for package installation as fallback (#16821)
* Use UV installer for package installation as fallback * Updates * Remove unwanted tests * Fix tests * Fix win tests * Fix win tests
1 parent 5c6e7fb commit c41df06

File tree

7 files changed

+342
-22
lines changed

7 files changed

+342
-22
lines changed

src/platform/interpreter/installer/channelManager.node.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,10 @@ export class InstallationChannelManager implements IInstallationChannelManager {
4444
// group by priority and pick supported from the highest priority
4545
installers.sort((a, b) => b.priority - a.priority);
4646
let currentPri = installers[0].priority;
47-
let uvInstaller: IModuleInstaller | undefined;
48-
for (const mi of installers) {
47+
// Check uv and Python ext installer only if there are no other options, hence exclude them initially.
48+
for (const mi of installers.filter(
49+
(m) => m.type !== ModuleInstallerType.UV && m.type !== ModuleInstallerType.PythonExt
50+
)) {
4951
if (mi.priority !== currentPri) {
5052
if (supportedInstallers.length > 0) {
5153
break; // return highest priority supported installers
@@ -54,16 +56,25 @@ export class InstallationChannelManager implements IInstallationChannelManager {
5456
currentPri = mi.priority;
5557
}
5658
if (await mi.isSupported(interpreter)) {
57-
if (mi.type === ModuleInstallerType.UV) {
58-
uvInstaller = mi;
59-
} else {
60-
supportedInstallers.push(mi);
61-
}
59+
supportedInstallers.push(mi);
6260
}
6361
}
64-
return supportedInstallers.length === 0 && uvInstaller
65-
? [uvInstaller] // If no supported installers, but UV is available, return it.
66-
: supportedInstallers; // Otherwise return the supported installers.
62+
63+
if (supportedInstallers.length > 0) {
64+
return supportedInstallers; // Return the highest priority supported installers.
65+
}
66+
67+
const pythonExtInstaller = installers.find((m) => m.type === ModuleInstallerType.PythonExt);
68+
const uvInstaller = installers.find((m) => m.type === ModuleInstallerType.UV);
69+
70+
if (pythonExtInstaller && (await pythonExtInstaller.isSupported(interpreter))) {
71+
return [pythonExtInstaller]; // If PythonExt is supported, return it.
72+
}
73+
if (uvInstaller && (await uvInstaller.isSupported(interpreter))) {
74+
return [uvInstaller]; // If UV is supported, return it.
75+
}
76+
77+
return []; // No supported installers found.
6778
}
6879

6980
public async showNoInstallersMessage(interpreter: PythonEnvironment): Promise<void> {

src/platform/interpreter/installer/pythonEnvsApiInstaller.node.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ export class PythonEnvsApiInstaller extends ModuleInstaller {
3434
}
3535

3636
public get type(): ModuleInstallerType {
37-
return ModuleInstallerType.UV;
37+
return ModuleInstallerType.PythonExt;
3838
}
3939

4040
public get displayName() {
41-
return 'Python Environment API';
41+
return 'Python Environment Installer';
4242
}
4343

4444
public get priority(): number {

src/platform/interpreter/installer/pythonEnvsApiInstaller.unit.test.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,8 @@ suite('Python Envs API Installer', () => {
5151

5252
teardown(() => disposables.clear());
5353

54-
test('Should have correct name', () => {
55-
assert.equal(installer.name, 'PythonEnvsApi');
56-
});
57-
58-
test('Should have correct display name', () => {
59-
assert.equal(installer.displayName, 'Python Environment API');
60-
});
61-
6254
test('Should have correct type', () => {
63-
assert.equal(installer.type, ModuleInstallerType.UV);
55+
assert.equal(installer.type, ModuleInstallerType.PythonExt);
6456
});
6557

6658
test('Should have high priority', () => {

src/platform/interpreter/installer/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ export enum ModuleInstallerType {
4444
Pip = 'Pip',
4545
Poetry = 'Poetry',
4646
Pipenv = 'Pipenv',
47-
UV = 'UV'
47+
UV = 'UV',
48+
PythonExt = 'PythonExt'
4849
}
4950

5051
export enum ProductType {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { inject, injectable } from 'inversify';
5+
import { IServiceContainer } from '../../ioc/types';
6+
import { ExecutionInstallArgs, ModuleInstaller } from './moduleInstaller.node';
7+
import { IProcessServiceFactory } from '../../common/process/types.node';
8+
import { ModuleInstallerType, ModuleInstallFlags } from './types';
9+
import { PythonEnvironment } from '../../pythonEnvironments/info';
10+
import { Environment } from '@vscode/python-extension';
11+
import { getInterpreterInfo } from '../helpers';
12+
13+
/**
14+
* Installer that uses the UV to manage packages.
15+
*/
16+
@injectable()
17+
export class UvInstaller extends ModuleInstaller {
18+
private isInstalledPromise: Promise<boolean>;
19+
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
20+
constructor(
21+
@inject(IServiceContainer) serviceContainer: IServiceContainer,
22+
@inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory
23+
) {
24+
super(serviceContainer);
25+
}
26+
27+
public get name(): string {
28+
return 'UvInstaller';
29+
}
30+
31+
public get type(): ModuleInstallerType {
32+
return ModuleInstallerType.UV;
33+
}
34+
35+
public get displayName() {
36+
return 'UV Installer';
37+
}
38+
39+
public get priority(): number {
40+
return 200;
41+
}
42+
43+
public async isSupported(interpreter: PythonEnvironment | Environment): Promise<boolean> {
44+
const env = await getInterpreterInfo(interpreter);
45+
if (!env) {
46+
return false;
47+
}
48+
if (this.isInstalledPromise) {
49+
return this.isInstalledPromise;
50+
}
51+
this.isInstalledPromise = this.isUvInstalled();
52+
return this.isInstalledPromise;
53+
}
54+
55+
protected async getExecutionArgs(
56+
moduleName: string,
57+
interpreter: PythonEnvironment | Environment,
58+
flags?: ModuleInstallFlags
59+
): Promise<ExecutionInstallArgs> {
60+
const env = await getInterpreterInfo(interpreter);
61+
if (!env) {
62+
throw new Error('Unable to get interpreter information');
63+
}
64+
const args = ['pip', 'install'];
65+
if (flags && flags & ModuleInstallFlags.upgrade) {
66+
args.push('--upgrade');
67+
}
68+
args.push('--python', env.executable.uri?.fsPath || env.path, moduleName);
69+
return {
70+
exe: 'uv',
71+
args
72+
};
73+
}
74+
75+
private async isUvInstalled(): Promise<boolean> {
76+
const processService = await this.processServiceFactory.create(undefined);
77+
try {
78+
const result = await processService.exec('uv', ['--version'], { throwOnStdErr: true, env: process.env });
79+
return !!result.stdout;
80+
} catch (error) {
81+
return false;
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)