Skip to content

Commit de4493f

Browse files
authored
Implement Python installation via uv (#1198)
Fixes #1172 Introduce functionality to prompt users for Python installation using uv when no environments are found. This includes updates to the UI and event tracking for the installation process.
1 parent cc45070 commit de4493f

File tree

10 files changed

+1239
-13
lines changed

10 files changed

+1239
-13
lines changed

src/common/localize.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ 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');
99102
export const venvVirtualEnvActive = l10n.t(
100103
'VIRTUAL_ENV is set for this VS Code session. Selection saved for new terminals only.',
101104
);
@@ -210,3 +213,34 @@ export namespace ActivationStrings {
210213
);
211214
export const activatingEnvironment = l10n.t('Activating environment');
212215
}
216+
217+
export namespace UvInstallStrings {
218+
export const noPythonFound = l10n.t('No Python installation found');
219+
export const installPythonPrompt = l10n.t(
220+
'No Python found. Would you like to install Python using uv? This will download and run an installer from https://astral.sh.',
221+
);
222+
export const installPythonAndUvPrompt = l10n.t(
223+
'No Python found. Would you like to install uv and use it to install Python? This will download and run an installer from https://astral.sh.',
224+
);
225+
export const installPython = l10n.t('Install Python');
226+
export const installingUv = l10n.t('Installing uv...');
227+
export const installingPython = l10n.t('Installing Python via uv...');
228+
export const installComplete = l10n.t('Python installed successfully');
229+
export function installCompleteWithDetails(version: string, path: string): string {
230+
return l10n.t('Python {0} installed successfully at {1}', version, path);
231+
}
232+
export function installCompleteWithPath(path: string): string {
233+
return l10n.t('Python installed successfully at {0}', path);
234+
}
235+
export const installFailed = l10n.t('Failed to install Python');
236+
export const uvInstallFailed = l10n.t('Failed to install uv');
237+
export const uvInstallRestartRequired = l10n.t(
238+
'uv was installed but may not be available in the current terminal. Please restart VS Code or open a new terminal and try again.',
239+
);
240+
export const dontAskAgain = l10n.t("Don't ask again");
241+
export const clickToInstallPython = l10n.t('No Python found, click to install');
242+
export const selectPythonVersion = l10n.t('Select Python version to install');
243+
export const installed = l10n.t('installed');
244+
export const fetchingVersions = l10n.t('Fetching available Python versions...');
245+
export const failedToFetchVersions = l10n.t('Failed to fetch available Python versions');
246+
}

src/common/tasks.apis.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
import { Task, TaskExecution, tasks } from 'vscode';
1+
import { Disposable, Task, TaskExecution, TaskProcessEndEvent, tasks } from 'vscode';
22

33
export async function executeTask(task: Task): Promise<TaskExecution> {
44
return tasks.executeTask(task);
55
}
6+
7+
export function onDidEndTaskProcess(
8+
listener: (e: TaskProcessEndEvent) => unknown,
9+
thisArgs?: unknown,
10+
disposables?: Disposable[],
11+
): Disposable {
12+
return tasks.onDidEndTaskProcess(listener, thisArgs, disposables);
13+
}

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' | 'uvNotOnPath' | 'pythonInstall' | 'findPath';
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';
@@ -176,20 +176,24 @@ export class NoPythonEnvTreeItem implements EnvTreeItem {
176176
private readonly tooltip?: string | MarkdownString,
177177
private readonly iconPath?: string | IconPath,
178178
) {
179-
const item = new TreeItem(
180-
this.parent.manager.supportsCreate
181-
? 'No environment found, click to create'
182-
: 'No python environments found.',
183-
TreeItemCollapsibleState.None,
184-
);
179+
// Use special message for system manager (Python installation)
180+
const isSystemManager = this.parent.manager.name === 'system';
181+
let label: string;
182+
if (this.parent.manager.supportsCreate) {
183+
label = isSystemManager ? UvInstallStrings.clickToInstallPython : VenvManagerStrings.noEnvClickToCreate;
184+
} else {
185+
label = VenvManagerStrings.noEnvFound;
186+
}
187+
188+
const item = new TreeItem(label, TreeItemCollapsibleState.None);
185189
item.contextValue = 'python-no-environment';
186190
item.description = this.description;
187191
item.tooltip = this.tooltip;
188192
item.iconPath = this.iconPath ?? new ThemeIcon('circle-slash');
189193
if (this.parent.manager.supportsCreate) {
190194
item.command = {
191195
command: 'python-envs.create',
192-
title: 'Create Environment',
196+
title: isSystemManager ? UvInstallStrings.installPython : VenvManagerStrings.createEnvironment,
193197
arguments: [this.parent],
194198
};
195199
}

src/managers/builtin/sysPythonManager.ts

Lines changed: 57 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, selectPythonVersionToInstall } from './uvPythonInstaller';
3134

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

7174
try {
7275
await this.internalRefresh(false, SysManagerStrings.sysManagerDiscovering);
76+
77+
// If no Python environments were found, offer to install via uv
78+
if (this.collection.length === 0) {
79+
const pythonPath = await promptInstallPythonViaUv('activation', this.log);
80+
if (pythonPath) {
81+
const resolved = await resolveSystemPythonEnvironmentPath(
82+
pythonPath,
83+
this.nativeFinder,
84+
this.api,
85+
this,
86+
);
87+
if (resolved) {
88+
this.collection.push(resolved);
89+
this.globalEnv = resolved;
90+
await setSystemEnvForGlobal(resolved.environmentPath.fsPath);
91+
this._onDidChangeEnvironments.fire([
92+
{ environment: resolved, kind: EnvironmentChangeKind.add },
93+
]);
94+
}
95+
}
96+
}
7397
} finally {
7498
this._initialized.resolve();
7599
}
@@ -220,6 +244,39 @@ export class SysPythonManager implements EnvironmentManager {
220244
return resolved;
221245
}
222246

247+
/**
248+
* Installs a global Python using uv.
249+
* This method shows a QuickPick to select the Python version, then installs it.
250+
*/
251+
async create(
252+
_scope: CreateEnvironmentScope,
253+
_options?: CreateEnvironmentOptions,
254+
): Promise<PythonEnvironment | undefined> {
255+
// Show QuickPick to select Python version
256+
const selectedVersion = await selectPythonVersionToInstall();
257+
if (!selectedVersion) {
258+
// User cancelled
259+
return undefined;
260+
}
261+
262+
const pythonPath = await installPythonWithUv(this.log, selectedVersion);
263+
264+
if (pythonPath) {
265+
// Resolve the installed Python using NativePythonFinder instead of full refresh
266+
const resolved = await resolveSystemPythonEnvironmentPath(pythonPath, this.nativeFinder, this.api, this);
267+
if (resolved) {
268+
// Add to collection, update global env, and fire change event
269+
this.collection.push(resolved);
270+
this.globalEnv = resolved;
271+
await setSystemEnvForGlobal(resolved.environmentPath.fsPath);
272+
this._onDidChangeEnvironments.fire([{ environment: resolved, kind: EnvironmentChangeKind.add }]);
273+
return resolved;
274+
}
275+
}
276+
277+
return undefined;
278+
}
279+
223280
async clearCache(): Promise<void> {
224281
await clearSystemEnvCache();
225282
}

0 commit comments

Comments
 (0)