Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve how to notify the user they don't have Python installed when installing the Python extension for the first time #19379

Merged
merged 5 commits into from
Jul 5, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,23 @@
"when": "workspacePlatform != webworker",
"steps": [
{
"id": "python.installPythonWin",
"id": "python.createPythonFile",
"title": "Create a Python file",
"description": "[Open](command:toSide:workbench.action.files.openFile) or [create](command:toSide:workbench.action.files.newUntitledFile) a Python file - make sure to save it as \".py\".\n[Create Python File](command:toSide:workbench.action.files.newUntitledFile)",
"media": {
"svg": "resources/walkthrough/open-folder.svg",
"altText": "Open a Python file or a folder with a Python project."
},
"when": ""
},
{
"id": "python.installPythonWin8",
"title": "Install Python",
"description": "The Python Extension requires Python to be installed. Install Python from the [Microsoft Store](https://aka.ms/AAd9rms).\n\n[Install Python](https://aka.ms/AAd9rms)\n",
"description": "The Python Extension requires Python to be installed. Install Python [from python.org](https://www.python.org/downloads).\n\n[Install Python](https://www.python.org/downloads)\n",
"media": {
"markdown": "resources/walkthrough/install-python-windows.md"
"markdown": "resources/walkthrough/install-python-windows-8.md"
},
"when": "workspacePlatform == windows"
"when": "workspacePlatform == windows && showInstallTile"
},
{
"id": "python.installPythonMac",
Expand All @@ -135,16 +145,6 @@
"when": "workspacePlatform == linux",
"command": "workbench.action.terminal.new"
},
{
"id": "python.createPythonFile",
"title": "Create a Python file",
"description": "[Open](command:toSide:workbench.action.files.openFile) or [create](command:toSide:python.createNewFile) a Python file - make sure to save it as \".py\".\n[Create Python File](command:toSide:python.createNewFile)",
"media": {
"svg": "resources/walkthrough/open-folder.svg",
"altText": "Open a Python file or a folder with a Python project."
},
"when": ""
},
{
"id": "python.selectInterpreter",
"title": "Select a Python Interpreter",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
## Install Python on Windows

If you don't have Python installed on your Windows machine, you can install it from the [Microsoft Store](https://aka.ms/AAd9rms).

To verify it's installed, create a new terminal (<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>`</kbd>) and try running the following command:

```
python --version
```

You should see something similar to the following:
```
Python 3.9.5
```
## Install Python on Windows

If you don't have Python installed on your Windows machine, you can install it [from python.org](https://www.python.org/downloads).

To verify it's installed, create a new terminal (<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>`</kbd>) and try running the following command:

```
python --version
```

You should see something similar to the following:
```
Python 3.9.5
```
For additional information about using Python on Windows, see [Using Python on Windows at Python.org](https://docs.python.org/3.10/using/windows.html).
2 changes: 2 additions & 0 deletions src/client/common/application/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type CommandsWithoutArgs = keyof ICommandNameWithoutArgumentTypeMapping;
* @interface ICommandNameWithoutArgumentTypeMapping
*/
interface ICommandNameWithoutArgumentTypeMapping {
[Commands.InstallPython]: [];
[Commands.ClearWorkspaceInterpreter]: [];
[Commands.Set_Interpreter]: [];
[Commands.Set_ShebangInterpreter]: [];
Expand Down Expand Up @@ -57,6 +58,7 @@ export type AllCommands = keyof ICommandNameArgumentTypeMapping;
export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgumentTypeMapping {
['vscode.openWith']: [Uri, string];
['workbench.action.quickOpen']: [string];
['workbench.action.openWalkthrough']: [string | { category: string; step: string }, boolean | undefined];
['workbench.extensions.installExtension']: [
Uri | 'ms-python.python',
(
Expand Down
1 change: 1 addition & 0 deletions src/client/common/application/contextKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

export enum ExtensionContextKey {
showInstallTile = 'showInstallTile',
HasFailedTests = 'hasFailedTests',
RefreshingTests = 'refreshingTests',
}
9 changes: 9 additions & 0 deletions src/client/common/application/walkThroughs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

export enum PythonWelcome {
name = 'pythonWelcome',
windowsInstallId = 'python.installPythonWin8',
linuxInstallId = 'python.installPythonLinux',
macOSInstallId = 'python.installPythonMac',
}
2 changes: 2 additions & 0 deletions src/client/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export namespace Commands {
export const LaunchTensorBoard = 'python.launchTensorBoard';
export const RefreshTensorBoard = 'python.refreshTensorBoard';
export const ReportIssue = 'python.reportIssue';
export const InstallPython = 'python.installPython';
}

// Look at https://microsoft.github.io/vscode-codicons/dist/codicon.html for other Octicon icon ids
Expand All @@ -72,6 +73,7 @@ export namespace Octicons {
export const Star = '$(star-full)';
export const Gear = '$(gear)';
export const Warning = '$(warning)';
export const Error = '$(error)';
}

export const DEFAULT_INTERPRETER_SETTING = 'python';
Expand Down
8 changes: 8 additions & 0 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,14 @@ export namespace Interpreters {
}

export namespace InterpreterQuickPickList {
export const noPythonInstalled = localize(
'InterpreterQuickPickList.noPythonInstalled',
'Python is not installed, please download and install it',
);
export const clickForInstructions = localize(
'InterpreterQuickPickList.clickForInstructions',
'Click for instructions...',
);
export const globalGroupName = localize('InterpreterQuickPickList.globalGroupName', 'Global');
export const workspaceGroupName = localize('InterpreterQuickPickList.workspaceGroupName', 'Workspace');
export const enterPath = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { inject, injectable } from 'inversify';
import { IExtensionSingleActivationService } from '../../../../activation/types';
import { ExtensionContextKey } from '../../../../common/application/contextKeys';
import { ICommandManager, IContextKeyManager } from '../../../../common/application/types';
import { PythonWelcome } from '../../../../common/application/walkThroughs';
import { Commands, PVSC_EXTENSION_ID } from '../../../../common/constants';
import { IBrowserService, IDisposableRegistry } from '../../../../common/types';
import { IPlatformService } from '../../../../common/platform/types';

@injectable()
export class InstallPythonCommand implements IExtensionSingleActivationService {
public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false };

constructor(
@inject(ICommandManager) private readonly commandManager: ICommandManager,
@inject(IContextKeyManager) private readonly contextManager: IContextKeyManager,
@inject(IBrowserService) private readonly browserService: IBrowserService,
@inject(IPlatformService) private readonly platformService: IPlatformService,
@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry,
) {}

public async activate(): Promise<void> {
this.disposables.push(this.commandManager.registerCommand(Commands.InstallPython, () => this._installPython()));
}

public async _installPython(): Promise<void> {
if (this.platformService.isWindows) {
const version = await this.platformService.getVersion();
if (version.major > 8) {
// OS is not Windows 8, ms-windows-store URIs are available:
// https://docs.microsoft.com/en-us/windows/uwp/launch-resume/launch-store-app
this.browserService.launch('ms-windows-store://pdp/?ProductId=9PJPW5LDXLZ5');
return;
}
}
this.showInstallTile();
}

private showInstallTile() {
this.contextManager.setContext(ExtensionContextKey.showInstallTile, true);
let step: string;
if (this.platformService.isWindows) {
step = PythonWelcome.windowsInstallId;
} else if (this.platformService.isLinux) {
step = PythonWelcome.linuxInstallId;
} else {
step = PythonWelcome.macOSInstallId;
}
this.commandManager.executeCommand(
'workbench.action.openWalkthrough',
{
category: `${PVSC_EXTENSION_ID}#${PythonWelcome.name}`,
step: `${PVSC_EXTENSION_ID}#${PythonWelcome.name}#${step}`,
},
false,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand {
alwaysShow: true,
};

private readonly noPythonInstalled: ISpecialQuickPickItem = {
label: `${Octicons.Error} ${InterpreterQuickPickList.noPythonInstalled}`,
detail: InterpreterQuickPickList.clickForInstructions,
alwaysShow: true,
};

constructor(
@inject(IApplicationShell) applicationShell: IApplicationShell,
@inject(IPathUtils) pathUtils: IPathUtils,
Expand Down Expand Up @@ -164,11 +170,12 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand {
} else if (selection.label === this.manualEntrySuggestion.label) {
sendTelemetryEvent(EventName.SELECT_INTERPRETER_ENTER_OR_FIND);
return this._enterOrBrowseInterpreterPath(input, state, suggestions);
} else if (selection.label === this.noPythonInstalled.label) {
this.commandManager.executeCommand(Commands.InstallPython);
} else {
sendTelemetryEvent(EventName.SELECT_INTERPRETER_SELECTED, undefined, { action: 'selected' });
state.path = (selection as IInterpreterQuickPickItem).path;
}

return undefined;
}

Expand All @@ -179,7 +186,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand {
suggestions.push(defaultInterpreterPathSuggestion);
}
const interpreterSuggestions = this.getSuggestions(resource);
this.setRecommendedItem(interpreterSuggestions, resource);
this.finalizeItems(interpreterSuggestions, resource);
suggestions.push(...interpreterSuggestions);
return suggestions;
}
Expand Down Expand Up @@ -214,7 +221,10 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand {
if (firstInterpreterSuggestion) {
return firstInterpreterSuggestion;
}
return suggestions[0];
const noPythonInstalledItem = suggestions.find(
(i) => isSpecialQuickPickItem(i) && i.label === this.noPythonInstalled.label,
);
return noPythonInstalledItem ?? suggestions[0];
}

private getDefaultInterpreterPathSuggestion(resource: Resource): ISpecialQuickPickItem | undefined {
Expand Down Expand Up @@ -286,6 +296,12 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand {
!areItemsGrouped,
);
if (envIndex === -1) {
const noPyIndex = updatedItems.findIndex(
(item) => isSpecialQuickPickItem(item) && item.label === this.noPythonInstalled.label,
);
if (noPyIndex !== -1) {
updatedItems.splice(noPyIndex, 1);
}
if (areItemsGrouped) {
addSeparatorIfApplicable(
updatedItems,
Expand All @@ -301,44 +317,57 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand {
if (envIndex !== -1 && event.new === undefined) {
updatedItems.splice(envIndex, 1);
}
this.setRecommendedItem(updatedItems, resource);
this.finalizeItems(updatedItems, resource);
return updatedItems;
}

private setRecommendedItem(items: QuickPickType[], resource: Resource) {
private finalizeItems(items: QuickPickType[], resource: Resource) {
const interpreterSuggestions = this.interpreterSelector.getSuggestions(resource, true);
if (!this.interpreterService.refreshPromise && interpreterSuggestions.length > 0) {
const suggestion = this.interpreterSelector.getRecommendedSuggestion(
interpreterSuggestions,
this.workspaceService.getWorkspaceFolder(resource)?.uri,
);
if (!suggestion) {
return;
}
const areItemsGrouped = items.find((item) => isSeparatorItem(item) && item.label === EnvGroups.Recommended);
const recommended = cloneDeep(suggestion);
recommended.label = `${Octicons.Star} ${recommended.label}`;
recommended.description = areItemsGrouped
? // No need to add a tag as "Recommended" group already exists.
recommended.description
: `${recommended.description ?? ''} - ${Common.recommended}`;
const index = items.findIndex(
(item) => isInterpreterQuickPickItem(item) && item.interpreter.id === recommended.interpreter.id,
);
if (index !== -1) {
items[index] = recommended;
}
items.forEach((item, i) => {
if (
isInterpreterQuickPickItem(item) &&
item.interpreter.path === 'python' &&
item.interpreter.envType === EnvironmentType.Conda
) {
if (!items[i].label.includes(Octicons.Warning)) {
items[i].label = `${Octicons.Warning} ${items[i].label}`;
if (!this.interpreterService.refreshPromise) {
if (interpreterSuggestions.length) {
this.setRecommendedItem(interpreterSuggestions, items, resource);
// Add warning label to certain environments
items.forEach((item, i) => {
if (
isInterpreterQuickPickItem(item) &&
item.interpreter.path === 'python' &&
item.interpreter.envType === EnvironmentType.Conda
) {
if (!items[i].label.includes(Octicons.Warning)) {
items[i].label = `${Octicons.Warning} ${items[i].label}`;
}
}
}
});
});
} else {
items.push(this.noPythonInstalled);
}
}
}

private setRecommendedItem(
interpreterSuggestions: IInterpreterQuickPickItem[],
items: QuickPickType[],
resource: Resource,
) {
const suggestion = this.interpreterSelector.getRecommendedSuggestion(
interpreterSuggestions,
this.workspaceService.getWorkspaceFolder(resource)?.uri,
);
if (!suggestion) {
return;
}
const areItemsGrouped = items.find((item) => isSeparatorItem(item) && item.label === EnvGroups.Recommended);
const recommended = cloneDeep(suggestion);
recommended.label = `${Octicons.Star} ${recommended.label}`;
recommended.description = areItemsGrouped
? // No need to add a tag as "Recommended" group already exists.
recommended.description
: `${recommended.description ?? ''} - ${Common.recommended}`;
const index = items.findIndex(
(item) => isInterpreterQuickPickItem(item) && item.interpreter.id === recommended.interpreter.id,
);
if (index !== -1) {
items[index] = recommended;
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/client/interpreter/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { InterpreterAutoSelectionService } from './autoSelection/index';
import { InterpreterAutoSelectionProxyService } from './autoSelection/proxy';
import { IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService } from './autoSelection/types';
import { EnvironmentTypeComparer } from './configuration/environmentTypeComparer';
import { InstallPythonCommand } from './configuration/interpreterSelector/commands/installPython';
import { ResetInterpreterCommand } from './configuration/interpreterSelector/commands/resetInterpreter';
import { SetInterpreterCommand } from './configuration/interpreterSelector/commands/setInterpreter';
import { SetShebangInterpreterCommand } from './configuration/interpreterSelector/commands/setShebangInterpreter';
Expand Down Expand Up @@ -40,6 +41,10 @@ import { VirtualEnvironmentPrompt } from './virtualEnvs/virtualEnvPrompt';
*/

export function registerInterpreterTypes(serviceManager: IServiceManager): void {
serviceManager.addSingleton<IExtensionSingleActivationService>(
IExtensionSingleActivationService,
InstallPythonCommand,
);
serviceManager.addSingleton<IExtensionSingleActivationService>(
IExtensionSingleActivationService,
SetInterpreterCommand,
Expand Down
Loading