Skip to content

Commit caa182e

Browse files
committed
feat: implement Python installation via uv
1 parent 451851c commit caa182e

File tree

6 files changed

+388
-11
lines changed

6 files changed

+388
-11
lines changed

src/common/localize.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ export namespace VenvManagerStrings {
9696
export const venvErrorNoBasePython = l10n.t('No base Python found');
9797
export const venvErrorNoPython3 = l10n.t('Did not find any base Python 3');
9898

99+
export const noEnvClickToCreate = l10n.t('No environment found, click to create');
100+
export const noEnvFound = l10n.t('No python environments found.');
101+
export const createEnvironment = l10n.t('Create Environment');
102+
99103
export const venvName = l10n.t('Enter a name for the virtual environment');
100104
export const venvNameErrorEmpty = l10n.t('Name cannot be empty');
101105
export const venvNameErrorExists = l10n.t('A folder with the same name already exists');
@@ -206,3 +210,19 @@ export namespace ActivationStrings {
206210
);
207211
export const activatingEnvironment = l10n.t('Activating environment');
208212
}
213+
214+
export namespace UvInstallStrings {
215+
export const noPythonFound = l10n.t('No Python installation found');
216+
export const installPythonPrompt = l10n.t('No Python found. Would you like to install Python using uv?');
217+
export const installPythonAndUvPrompt = l10n.t(
218+
'No Python found. Would you like to install uv and use it to install Python?',
219+
);
220+
export const installPython = l10n.t('Install Python');
221+
export const installingUv = l10n.t('Installing uv...');
222+
export const installingPython = l10n.t('Installing Python via uv...');
223+
export const installComplete = l10n.t('Python installed successfully');
224+
export const installFailed = l10n.t('Failed to install Python');
225+
export const uvInstallFailed = l10n.t('Failed to install uv');
226+
export const dontAskAgain = l10n.t("Don't ask again");
227+
export const clickToInstallPython = l10n.t('No Python found, click to install');
228+
}

src/common/telemetry/constants.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ export enum EventNames {
1010
VENV_USING_UV = 'VENV.USING_UV',
1111
VENV_CREATION = 'VENV.CREATION',
1212

13+
UV_PYTHON_INSTALL_PROMPTED = 'UV.PYTHON_INSTALL_PROMPTED',
14+
UV_PYTHON_INSTALL_STARTED = 'UV.PYTHON_INSTALL_STARTED',
15+
UV_PYTHON_INSTALL_COMPLETED = 'UV.PYTHON_INSTALL_COMPLETED',
16+
UV_PYTHON_INSTALL_FAILED = 'UV.PYTHON_INSTALL_FAILED',
17+
1318
PACKAGE_MANAGEMENT = 'PACKAGE_MANAGEMENT',
1419
ADD_PROJECT = 'ADD_PROJECT',
1520
/**
@@ -83,15 +88,49 @@ export interface IEventNamePropertyMapping {
8388
/* __GDPR__
8489
"venv.using_uv": {"owner": "eleanorjboyd" }
8590
*/
86-
[EventNames.VENV_USING_UV]: never | undefined /* __GDPR__
91+
[EventNames.VENV_USING_UV]: never | undefined;
92+
93+
/* __GDPR__
8794
"venv.creation": {
8895
"creationType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
8996
}
90-
*/;
97+
*/
9198
[EventNames.VENV_CREATION]: {
9299
creationType: 'quick' | 'custom';
93100
};
94101

102+
/* __GDPR__
103+
"uv.python_install_prompted": {
104+
"trigger": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }
105+
}
106+
*/
107+
[EventNames.UV_PYTHON_INSTALL_PROMPTED]: {
108+
trigger: 'activation' | 'createEnvironment';
109+
};
110+
111+
/* __GDPR__
112+
"uv.python_install_started": {
113+
"uvAlreadyInstalled": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }
114+
}
115+
*/
116+
[EventNames.UV_PYTHON_INSTALL_STARTED]: {
117+
uvAlreadyInstalled: boolean;
118+
};
119+
120+
/* __GDPR__
121+
"uv.python_install_completed": {"owner": "karthiknadig" }
122+
*/
123+
[EventNames.UV_PYTHON_INSTALL_COMPLETED]: never | undefined;
124+
125+
/* __GDPR__
126+
"uv.python_install_failed": {
127+
"stage": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }
128+
}
129+
*/
130+
[EventNames.UV_PYTHON_INSTALL_FAILED]: {
131+
stage: 'uvInstall' | 'pythonInstall';
132+
};
133+
95134
/* __GDPR__
96135
"package_management": {
97136
"managerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },

src/features/views/treeViewItems.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Command, MarkdownString, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode';
22
import { EnvironmentGroupInfo, IconPath, Package, PythonEnvironment, PythonProject } from '../../api';
3-
import { EnvViewStrings } from '../../common/localize';
3+
import { EnvViewStrings, UvInstallStrings, VenvManagerStrings } from '../../common/localize';
44
import { InternalEnvironmentManager, InternalPackageManager } from '../../internal.api';
55
import { isActivatableEnvironment } from '../common/activation';
66
import { removable } from './utils';
@@ -115,20 +115,24 @@ export class NoPythonEnvTreeItem implements EnvTreeItem {
115115
private readonly tooltip?: string | MarkdownString,
116116
private readonly iconPath?: string | IconPath,
117117
) {
118-
const item = new TreeItem(
119-
this.parent.manager.supportsCreate
120-
? 'No environment found, click to create'
121-
: 'No python environments found.',
122-
TreeItemCollapsibleState.None,
123-
);
118+
// Use special message for system manager (Python installation)
119+
const isSystemManager = this.parent.manager.name === 'system';
120+
let label: string;
121+
if (this.parent.manager.supportsCreate) {
122+
label = isSystemManager ? UvInstallStrings.clickToInstallPython : VenvManagerStrings.noEnvClickToCreate;
123+
} else {
124+
label = VenvManagerStrings.noEnvFound;
125+
}
126+
127+
const item = new TreeItem(label, TreeItemCollapsibleState.None);
124128
item.contextValue = 'python-no-environment';
125129
item.description = this.description;
126130
item.tooltip = this.tooltip;
127131
item.iconPath = this.iconPath ?? new ThemeIcon('circle-slash');
128132
if (this.parent.manager.supportsCreate) {
129133
item.command = {
130134
command: 'python-envs.create',
131-
title: 'Create Environment',
135+
title: isSystemManager ? UvInstallStrings.installPython : VenvManagerStrings.createEnvironment,
132136
arguments: [this.parent],
133137
};
134138
}

src/managers/builtin/sysPythonManager.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as path from 'path';
22
import { EventEmitter, LogOutputChannel, MarkdownString, ProgressLocation, ThemeIcon, Uri, window } from 'vscode';
33
import {
4+
CreateEnvironmentOptions,
5+
CreateEnvironmentScope,
46
DidChangeEnvironmentEventArgs,
57
DidChangeEnvironmentsEventArgs,
68
EnvironmentChangeKind,
@@ -28,6 +30,7 @@ import {
2830
setSystemEnvForWorkspaces,
2931
} from './cache';
3032
import { refreshPythons, resolveSystemPythonEnvironmentPath } from './utils';
33+
import { installPythonWithUv, promptInstallPythonViaUv } from './uvPythonInstaller';
3134

3235
export class SysPythonManager implements EnvironmentManager {
3336
private collection: PythonEnvironment[] = [];
@@ -70,6 +73,11 @@ export class SysPythonManager implements EnvironmentManager {
7073

7174
await this.internalRefresh(false, SysManagerStrings.sysManagerDiscovering);
7275

76+
// If no Python environments were found, offer to install via uv
77+
if (this.collection.length === 0) {
78+
promptInstallPythonViaUv('activation', this.api, this.log);
79+
}
80+
7381
this._initialized.resolve();
7482
}
7583

@@ -218,6 +226,25 @@ export class SysPythonManager implements EnvironmentManager {
218226
return resolved;
219227
}
220228

229+
/**
230+
* Installs a global Python using uv.
231+
* This method installs uv if not present, then uses it to install Python.
232+
*/
233+
async create(
234+
_scope: CreateEnvironmentScope,
235+
_options?: CreateEnvironmentOptions,
236+
): Promise<PythonEnvironment | undefined> {
237+
const success = await installPythonWithUv(this.api, this.log);
238+
239+
if (success) {
240+
// Return the latest Python environment after installation
241+
// The installPythonWithUv function already refreshes environments
242+
return getLatest(this.collection);
243+
}
244+
245+
return undefined;
246+
}
247+
221248
async clearCache(): Promise<void> {
222249
await clearSystemEnvCache();
223250
}

0 commit comments

Comments
 (0)