Skip to content

Implement Python installation via uv#1198

Draft
karthiknadig wants to merge 5 commits intomainfrom
bold-unicorn
Draft

Implement Python installation via uv#1198
karthiknadig wants to merge 5 commits intomainfrom
bold-unicorn

Conversation

@karthiknadig
Copy link
Member

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.

@karthiknadig karthiknadig added the feature-request Request for new features or functionality label Feb 6, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request implements Python installation via uv when no Python environments are found, addressing issue #1172. The feature prompts users to install Python using the uv tool, which can install Python without requiring Python to be present first. This streamlines the experience for new Python users or those setting up fresh machines.

Changes:

  • Added Python installation functionality through uv with cross-platform support (Windows, macOS, Linux)
  • Integrated installation prompts at two trigger points: extension activation and environment creation
  • Added persistent "Don't ask again" state management for user preferences
  • Implemented comprehensive telemetry tracking for installation flow

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/managers/builtin/uvPythonInstaller.ts New module implementing uv and Python installation logic with platform-specific commands, task execution, and user prompts
src/test/managers/builtin/uvPythonInstaller.unit.test.ts Unit tests for prompt behavior, persistent state management, and telemetry
src/managers/builtin/sysPythonManager.ts Added prompt during initialization and create method to install Python globally via uv
src/managers/builtin/venvManager.ts Added prompt during venv creation when no global Python environments exist
src/features/views/treeViewItems.ts Updated UI labels for system manager to show "click to install" when no Python found
src/common/telemetry/constants.ts Added four telemetry events for tracking installation flow
src/common/localize.ts Added localized strings for installation UI and messages
Comments suppressed due to low confidence (5)

src/managers/builtin/uvPythonInstaller.ts:253

  • Direct usage of window.showErrorMessage and window.showInformationMessage from the vscode module should be replaced with the wrapper functions from src/common/window.apis.ts. While one wrapper is already imported and used for the prompt (line 189), these direct calls should be replaced with the imported showErrorMessage from window.apis for consistency.
                    window.showErrorMessage(UvInstallStrings.uvInstallFailed);
                    return false;
                }
            }

            // Step 2: Install Python via uv
            const pythonSuccess = await installPythonViaUv(log);
            if (!pythonSuccess) {
                sendTelemetryEvent(EventNames.UV_PYTHON_INSTALL_FAILED, undefined, { stage: 'pythonInstall' });
                window.showErrorMessage(UvInstallStrings.installFailed);
                return false;
            }

            // Step 3: Refresh environments to detect newly installed Python
            traceInfo('Refreshing environments after Python installation...');
            await api.refreshEnvironments(undefined);

            sendTelemetryEvent(EventNames.UV_PYTHON_INSTALL_COMPLETED);
            window.showInformationMessage(UvInstallStrings.installComplete);

src/managers/builtin/uvPythonInstaller.ts:36

  • The isCommandAvailable function doesn't handle the case where the spawned process might hang indefinitely. If the --version command for curl or wget doesn't respond, the Promise will never resolve, causing getUvInstallCommand to hang. Consider adding a timeout mechanism using setTimeout to reject the promise after a reasonable duration (e.g., 5 seconds).
async function isCommandAvailable(command: string): Promise<boolean> {
    return new Promise((resolve) => {
        const proc = spawnProcess(command, ['--version']);
        proc.on('error', () => resolve(false));
        proc.on('exit', (code) => resolve(code === 0));
    });
}

src/managers/builtin/uvPythonInstaller.ts:95

  • Direct usage of tasks.onDidEndTaskProcess from the vscode module should ideally use a wrapper function for consistency with the codebase pattern. However, no wrapper exists yet in src/common/tasks.apis.ts. Consider adding a wrapper function there and using it here, similar to how executeTask is wrapped.
    const disposable = tasks.onDidEndTaskProcess((e) => {

src/managers/builtin/sysPythonManager.ts:242

  • The comment states "The installPythonWithUv function already refreshes environments", but there's a potential timing issue. The installPythonWithUv function calls api.refreshEnvironments(undefined) which likely triggers sysPythonManager.refresh(), updating sysPythonManager.collection. However, the create method immediately reads from this.collection which might not yet reflect the newly installed Python if the refresh is still in progress or hasn't propagated. Consider awaiting a small delay or re-fetching environments explicitly within the create method after the installPythonWithUv call to ensure the collection is up-to-date.
        const success = await installPythonWithUv(this.api, this.log);

        if (success) {
            // Return the latest Python environment after installation
            // The installPythonWithUv function already refreshes environments
            return getLatest(this.collection);

src/test/managers/builtin/uvPythonInstaller.unit.test.ts:123

  • Missing test case for when user clicks "Install Python" button. The tests cover the "Don't ask again" and dismiss scenarios, but there's no test verifying that when the user selects UvInstallStrings.installPython, the function calls installPythonWithUv and returns its result. This is a critical flow that should be tested to ensure proper integration.
suite('uvPythonInstaller - promptInstallPythonViaUv', () => {
    let mockLog: LogOutputChannel;
    let mockApi: { refreshEnvironments: sinon.SinonStub };
    let isUvInstalledStub: sinon.SinonStub;
    let showInformationMessageStub: sinon.SinonStub;
    let sendTelemetryEventStub: sinon.SinonStub;
    let mockState: { get: sinon.SinonStub; set: sinon.SinonStub; clear: sinon.SinonStub };

    setup(() => {
        mockLog = createMockLogOutputChannel();
        mockApi = { refreshEnvironments: sinon.stub().resolves() };

        mockState = {
            get: sinon.stub(),
            set: sinon.stub().resolves(),
            clear: sinon.stub().resolves(),
        };
        sinon.stub(persistentState, 'getGlobalPersistentState').resolves(mockState);
        isUvInstalledStub = sinon.stub(helpers, 'isUvInstalled');
        showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage');
        sendTelemetryEventStub = sinon.stub(telemetrySender, 'sendTelemetryEvent');
    });

    teardown(() => {
        sinon.restore();
    });

    test('should return false when "Don\'t ask again" is set', async () => {
        mockState.get.resolves(true);

        const result = await promptInstallPythonViaUv('activation', mockApi as any, mockLog);

        assert.strictEqual(result, false);
        assert(showInformationMessageStub.notCalled, 'Should not show message when dont ask again is set');
        assert(sendTelemetryEventStub.notCalled, 'Should not send telemetry when skipping prompt');
    });

    test('should show correct prompt when uv is installed', async () => {
        mockState.get.resolves(false);
        isUvInstalledStub.resolves(true);
        showInformationMessageStub.resolves(undefined); // User dismissed

        await promptInstallPythonViaUv('activation', mockApi as any, mockLog);

        assert(
            showInformationMessageStub.calledWith(
                UvInstallStrings.installPythonPrompt,
                UvInstallStrings.installPython,
                UvInstallStrings.dontAskAgain,
            ),
            'Should show install Python prompt when uv is installed',
        );
    });

    test('should show correct prompt when uv is NOT installed', async () => {
        mockState.get.resolves(false);
        isUvInstalledStub.resolves(false);
        showInformationMessageStub.resolves(undefined); // User dismissed

        await promptInstallPythonViaUv('activation', mockApi as any, mockLog);

        assert(
            showInformationMessageStub.calledWith(
                UvInstallStrings.installPythonAndUvPrompt,
                UvInstallStrings.installPython,
                UvInstallStrings.dontAskAgain,
            ),
            'Should show install Python AND uv prompt when uv is not installed',
        );
    });

    test('should set persistent state when user clicks "Don\'t ask again"', async () => {
        mockState.get.resolves(false);
        isUvInstalledStub.resolves(true);
        showInformationMessageStub.resolves(UvInstallStrings.dontAskAgain);

        const result = await promptInstallPythonViaUv('activation', mockApi as any, mockLog);

        assert.strictEqual(result, false);
        assert(mockState.set.calledWith('python-envs:uv:UV_INSTALL_PYTHON_DONT_ASK', true), 'Should set dont ask flag');
    });

    test('should return false when user dismisses the dialog', async () => {
        mockState.get.resolves(false);
        isUvInstalledStub.resolves(true);
        showInformationMessageStub.resolves(undefined); // User dismissed

        const result = await promptInstallPythonViaUv('activation', mockApi as any, mockLog);

        assert.strictEqual(result, false);
    });

    test('should send telemetry with correct trigger', async () => {
        mockState.get.resolves(false);
        isUvInstalledStub.resolves(true);
        showInformationMessageStub.resolves(undefined);

        await promptInstallPythonViaUv('createEnvironment', mockApi as any, mockLog);

        assert(
            sendTelemetryEventStub.calledWith(EventNames.UV_PYTHON_INSTALL_PROMPTED, undefined, {
                trigger: 'createEnvironment',
            }),
            'Should send telemetry with createEnvironment trigger',
        );
    });
});

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Comment on lines +1709 to +1711
// Return a simple unique ID based on command
const cmd = typeof this._command === 'string' ? this._command : (this._command?.value ?? '');
return `shell-${cmd}-${Date.now()}`;
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ShellExecution.computeId() now uses Date.now(), which makes IDs non-deterministic and can introduce flaky tests / inconsistent behavior when the same command is executed multiple times. Consider generating a stable ID (e.g., hash of command + args) or using a monotonic counter scoped to the mock instead.

Copilot uses AI. Check for mistakes.
const result = await promptInstallPythonViaUv('activation', mockApi as PythonEnvironmentApi, mockLog);

assert.strictEqual(result, false);
assert(mockState.set.calledWith('python-envs:uv:UV_INSTALL_PYTHON_DONT_ASK', true), 'Should set dont ask flag');
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These assertions hardcode the persistent-state key string. To avoid brittle tests if the key changes, consider exporting the key constant from uvPythonInstaller.ts (or exposing a small getter) and reusing it in tests.

Suggested change
assert(mockState.set.calledWith('python-envs:uv:UV_INSTALL_PYTHON_DONT_ASK', true), 'Should set dont ask flag');
assert(
mockState.set.calledWith(sinon.match.any, true),
'Should set dont ask flag',
);

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +101
export const noEnvFound = l10n.t('No python environments found.');
export const createEnvironment = l10n.t('Create Environment');
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User-facing string uses "python" in lowercase. For consistency with other UI strings and product naming, this should be capitalized to "Python".

Copilot uses AI. Check for mistakes.
Comment on lines +238 to +252
// Step 1: Install uv if not present
if (!uvInstalled) {
traceInfo('uv not found, installing uv first...');

const uvSuccess = await installUv(log);
if (!uvSuccess) {
sendTelemetryEvent(EventNames.UV_PYTHON_INSTALL_FAILED, undefined, { stage: 'uvInstall' });
showErrorMessage(UvInstallStrings.uvInstallFailed);
return false;
}
}

// Step 2: Install Python via uv
const pythonSuccess = await installPythonViaUv(log);
if (!pythonSuccess) {
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After successfully installing uv, the next step immediately runs uv python install. Depending on how uv is installed, the new uv executable may not be on PATH for subsequent tasks/shells (install script often updates shell profile). Consider re-checking isUvInstalled() (or resolving uv’s install location) after installUv() and showing an actionable error (e.g., restart VS Code / open a new terminal) if uv still isn’t discoverable, to avoid a confusing failure at the Python-install stage.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature-request Request for new features or functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Install Python via uv when no Python is found

1 participant