Skip to content

Commit 8c2a8ff

Browse files
CopilotDonJayamanne
andcommitted
Implement UV environment support for Jupyter installer system
Co-authored-by: DonJayamanne <[email protected]>
1 parent 3a095e9 commit 8c2a8ff

File tree

8 files changed

+204
-8
lines changed

8 files changed

+204
-8
lines changed

src/platform/interpreter/helpers.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ const environmentTypes = [
8181
EnvironmentType.Pyenv,
8282
EnvironmentType.Venv,
8383
EnvironmentType.VirtualEnv,
84-
EnvironmentType.VirtualEnvWrapper
84+
EnvironmentType.VirtualEnvWrapper,
85+
EnvironmentType.UV
8586
];
8687

8788
export function getEnvironmentType(interpreter: { id: string }): EnvironmentType {
@@ -93,6 +94,11 @@ function getEnvironmentTypeImpl(env: Environment): EnvironmentType {
9394
return EnvironmentType.Conda;
9495
}
9596

97+
// Check for UV environment by looking for uv tools
98+
if (env.tools.some((tool) => tool.toLowerCase() === 'uv')) {
99+
return EnvironmentType.UV;
100+
}
101+
96102
// Map the Python env tool to a Jupyter environment type.
97103
const orderOrEnvs: [pythonEnvTool: KnownEnvironmentTools, JupyterEnv: EnvironmentType][] = [
98104
['Conda', EnvironmentType.Conda],

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { IPlatformService } from '../../common/platform/types';
88
import { Installer } from '../../common/utils/localize';
99
import { IServiceContainer } from '../../ioc/types';
1010
import { IInstallationChannelManager, IModuleInstaller, Product } from './types';
11-
import { Uri, env, window } from 'vscode';
11+
import { Uri, env, window, l10n } from 'vscode';
1212
import { getEnvironmentType } from '../helpers';
1313

1414
/**
@@ -61,8 +61,26 @@ export class InstallationChannelManager implements IInstallationChannelManager {
6161

6262
public async showNoInstallersMessage(interpreter: PythonEnvironment): Promise<void> {
6363
const envType = getEnvironmentType(interpreter);
64+
let message: string;
65+
let searchTerm: string;
66+
67+
switch (envType) {
68+
case EnvironmentType.Conda:
69+
message = Installer.noCondaOrPipInstaller;
70+
searchTerm = 'Install Pip Conda';
71+
break;
72+
case EnvironmentType.UV:
73+
message = l10n.t('There is no UV installer available in the selected environment.');
74+
searchTerm = 'Install UV Python';
75+
break;
76+
default:
77+
message = Installer.noPipInstaller;
78+
searchTerm = 'Install Pip';
79+
break;
80+
}
81+
6482
const result = await window.showErrorMessage(
65-
envType === EnvironmentType.Conda ? Installer.noCondaOrPipInstaller : Installer.noPipInstaller,
83+
message,
6684
{ modal: true },
6785
Installer.searchForHelp
6886
);
@@ -71,9 +89,7 @@ export class InstallationChannelManager implements IInstallationChannelManager {
7189
const osName = platform.isWindows ? 'Windows' : platform.isMac ? 'MacOS' : 'Linux';
7290
void env.openExternal(
7391
Uri.parse(
74-
`https://www.bing.com/search?q=Install Pip ${osName} ${
75-
envType === EnvironmentType.Conda ? 'Conda' : ''
76-
}`
92+
`https://www.bing.com/search?q=${searchTerm} ${osName}`
7793
)
7894
);
7995
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,12 @@ export class PipInstaller extends ModuleInstaller {
4141
}
4242
public async isSupported(interpreter: PythonEnvironment | Environment): Promise<boolean> {
4343
const envType = getEnvironmentType(interpreter);
44-
// Skip this on conda, poetry, and pipenv environments
44+
// Skip this on conda, poetry, pipenv, and UV environments
4545
switch (envType) {
4646
case EnvironmentType.Conda:
4747
case EnvironmentType.Pipenv:
4848
case EnvironmentType.Poetry:
49+
case EnvironmentType.UV:
4950
return false;
5051
}
5152

src/platform/interpreter/installer/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ export enum ModuleInstallerType {
4343
Conda = 'Conda',
4444
Pip = 'Pip',
4545
Poetry = 'Poetry',
46-
Pipenv = 'Pipenv'
46+
Pipenv = 'Pipenv',
47+
UV = 'UV'
4748
}
4849

4950
export enum ProductType {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { inject, injectable } from 'inversify';
5+
import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info';
6+
import { ExecutionInstallArgs, ModuleInstaller } from './moduleInstaller.node';
7+
import { ModuleInstallerType, ModuleInstallFlags } from './types';
8+
import { IServiceContainer } from '../../ioc/types';
9+
import { Environment } from '@vscode/python-extension';
10+
import { getEnvironmentType } from '../helpers';
11+
import { workspace } from 'vscode';
12+
13+
/**
14+
* Installer for UV environments.
15+
*/
16+
@injectable()
17+
export class UvInstaller extends ModuleInstaller {
18+
constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
19+
super(serviceContainer);
20+
}
21+
22+
public get name(): string {
23+
return 'UV';
24+
}
25+
26+
public get type(): ModuleInstallerType {
27+
return ModuleInstallerType.UV;
28+
}
29+
30+
public get displayName() {
31+
return 'UV';
32+
}
33+
34+
public get priority(): number {
35+
return 10;
36+
}
37+
38+
public async isSupported(interpreter: PythonEnvironment | Environment): Promise<boolean> {
39+
// Check if this is a UV environment
40+
const envType = getEnvironmentType(interpreter);
41+
if (envType === EnvironmentType.UV) {
42+
return true;
43+
}
44+
45+
// For now, we'll be conservative and only support explicitly detected UV environments
46+
// In the future, we could add more sophisticated detection like:
47+
// - Checking for pyproject.toml with [tool.uv] configuration
48+
// - Checking if 'uv' command is available in PATH
49+
// - Checking if the interpreter path suggests UV management
50+
return false;
51+
}
52+
53+
protected async getExecutionArgs(
54+
moduleName: string,
55+
interpreter: PythonEnvironment | Environment,
56+
flags: ModuleInstallFlags = 0
57+
): Promise<ExecutionInstallArgs> {
58+
const args: string[] = [];
59+
const proxy = workspace.getConfiguration('http').get('proxy', '');
60+
if (proxy.length > 0) {
61+
args.push('--proxy');
62+
args.push(proxy);
63+
}
64+
65+
// Use UV pip install syntax
66+
args.push('pip', 'install');
67+
68+
if (flags & ModuleInstallFlags.upgrade) {
69+
args.push('--upgrade');
70+
}
71+
if (flags & ModuleInstallFlags.reInstall) {
72+
args.push('--force-reinstall');
73+
}
74+
if (flags & ModuleInstallFlags.updateDependencies) {
75+
args.push('--upgrade-strategy', 'eager');
76+
}
77+
78+
args.push(moduleName);
79+
80+
return {
81+
exe: 'uv',
82+
args
83+
};
84+
}
85+
}
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 { mock } from 'ts-mockito';
5+
import { expect } from 'chai';
6+
import { UvInstaller } from './uvInstaller.node';
7+
import { IServiceContainer } from '../../ioc/types';
8+
import { ModuleInstallerType, ModuleInstallFlags } from './types';
9+
import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info';
10+
import { Environment } from '@vscode/python-extension';
11+
12+
// Mock helper to simulate getEnvironmentType
13+
const mockGetEnvironmentType = (envType: EnvironmentType) => {
14+
// This would need to be properly mocked in a real test environment
15+
return envType;
16+
};
17+
18+
describe('UV Installer', () => {
19+
let installer: UvInstaller;
20+
let serviceContainer: IServiceContainer;
21+
22+
beforeEach(() => {
23+
serviceContainer = mock<IServiceContainer>();
24+
installer = new UvInstaller(serviceContainer);
25+
});
26+
27+
it('Should have correct properties', () => {
28+
expect(installer.name).to.equal('UV');
29+
expect(installer.displayName).to.equal('UV');
30+
expect(installer.type).to.equal(ModuleInstallerType.UV);
31+
expect(installer.priority).to.equal(10);
32+
});
33+
34+
it('Should support UV environments', async () => {
35+
const mockInterpreter: PythonEnvironment = {
36+
id: 'test-uv-env',
37+
uri: { fsPath: '/path/to/uv/env' } as any
38+
};
39+
40+
// This test would need to be enhanced with proper mocking
41+
// For now, it demonstrates the expected structure
42+
expect(installer.isSupported).to.be.a('function');
43+
});
44+
45+
it('Should generate correct execution args for UV pip install', async () => {
46+
const mockInterpreter: Environment = {
47+
id: 'test-uv-env',
48+
path: '/path/to/uv/env',
49+
tools: ['uv'],
50+
} as any;
51+
52+
// Test basic installation
53+
const args = await (installer as any).getExecutionArgs('numpy', mockInterpreter, ModuleInstallFlags.None);
54+
55+
expect(args.exe).to.equal('uv');
56+
expect(args.args).to.include('pip');
57+
expect(args.args).to.include('install');
58+
expect(args.args).to.include('numpy');
59+
});
60+
61+
it('Should handle upgrade flag correctly', async () => {
62+
const mockInterpreter: Environment = {
63+
id: 'test-uv-env',
64+
path: '/path/to/uv/env',
65+
tools: ['uv'],
66+
} as any;
67+
68+
const args = await (installer as any).getExecutionArgs('numpy', mockInterpreter, ModuleInstallFlags.upgrade);
69+
70+
expect(args.args).to.include('--upgrade');
71+
});
72+
73+
it('Should handle reinstall flag correctly', async () => {
74+
const mockInterpreter: Environment = {
75+
id: 'test-uv-env',
76+
path: '/path/to/uv/env',
77+
tools: ['uv'],
78+
} as any;
79+
80+
const args = await (installer as any).getExecutionArgs('numpy', mockInterpreter, ModuleInstallFlags.reInstall);
81+
82+
expect(args.args).to.include('--force-reinstall');
83+
});
84+
});

src/platform/interpreter/serviceRegistry.node.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { CondaInstaller } from './installer/condaInstaller.node';
1616
import { PipEnvInstaller } from './installer/pipEnvInstaller.node';
1717
import { PipInstaller } from './installer/pipInstaller.node';
1818
import { PoetryInstaller } from './installer/poetryInstaller.node';
19+
import { UvInstaller } from './installer/uvInstaller.node';
1920
import { ProductInstaller } from './installer/productInstaller.node';
2021
import { DataScienceProductPathService } from './installer/productPath.node';
2122
import { ProductService } from './installer/productService.node';
@@ -78,6 +79,7 @@ export function registerTypes(serviceManager: IServiceManager) {
7879
serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PipInstaller);
7980
serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PipEnvInstaller);
8081
serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PoetryInstaller);
82+
serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, UvInstaller);
8183
serviceManager.addSingleton<IInstallationChannelManager>(IInstallationChannelManager, InstallationChannelManager);
8284
serviceManager.addSingleton<IProductService>(IProductService, ProductService);
8385
serviceManager.addSingleton<IInstaller>(IInstaller, ProductInstaller);

src/platform/pythonEnvironments/info/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export enum EnvironmentType {
1616
Venv = 'Venv',
1717
Poetry = 'Poetry',
1818
VirtualEnvWrapper = 'VirtualEnvWrapper',
19+
UV = 'UV',
1920
}
2021

2122
/**

0 commit comments

Comments
 (0)