Skip to content

Commit d5ddcba

Browse files
Merge remote-tracking branch 'Parent/main' into Current
2 parents 56e41b0 + 22be07b commit d5ddcba

16 files changed

+1762
-10
lines changed

.prettierignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Ignore the pythonEnvironments/ folder because we use ESLint there instead
2+
src/client/pythonEnvironments/*
3+
src/test/pythonEnvironments/*

.prettierrc.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module.exports = {
2+
singleQuote: true,
3+
printWidth: 120,
4+
tabWidth: 4,
5+
endOfLine: 'auto',
6+
trailingComma: 'none',
7+
overrides: [
8+
{
9+
files: ['*.yml', '*.yaml'],
10+
options: {
11+
tabWidth: 2
12+
}
13+
},
14+
{
15+
files: ['**/datascience/serviceRegistry.ts'],
16+
options: {
17+
printWidth: 240
18+
}
19+
}
20+
]
21+
};

package.json

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
{
2-
"dependencies": {},
3-
"description": "VS Code module for installing python packages",
4-
"devDependencies": {},
5-
"name": "@vscode/python-installer",
6-
"scripts": {
7-
"compile": "tsc -p ./",
8-
"compilewatch": "tsc -watch -p ./",
9-
"download-api": "vscode-dts dev",
10-
"postdownload-api": "vscode-dts main"
11-
}
2+
"name": "@vscode/python-installer",
3+
"version": "0.1.0",
4+
"description": "VS Code module for installing python packages",
5+
"scripts": {
6+
"compile": "tsc -p ./",
7+
"compilewatch": "tsc -watch -p ./",
8+
"download-api": "vscode-dts dev",
9+
"postdownload-api": "vscode-dts main"
10+
},
11+
"author": "Visual Studio Code Team",
12+
"license": "MIT",
13+
"dependencies": {
14+
},
15+
"devDependencies": {
16+
}
1217
}

src/channelManager.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { inject, injectable } from 'inversify';
5+
import { Uri } from 'vscode';
6+
import { IInterpreterService } from '../../interpreter/contracts';
7+
import { IServiceContainer } from '../../ioc/types';
8+
import { EnvironmentType } from '../../pythonEnvironments/info';
9+
import { IApplicationShell } from '../application/types';
10+
import { IPlatformService } from '../platform/types';
11+
import { Product } from '../types';
12+
import { Installer } from '../utils/localize';
13+
import { isResource } from '../utils/misc';
14+
import { ProductNames } from './productNames';
15+
import { IInstallationChannelManager, IModuleInstaller, InterpreterUri } from './types';
16+
17+
@injectable()
18+
export class InstallationChannelManager implements IInstallationChannelManager {
19+
constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {}
20+
21+
public async getInstallationChannel(
22+
product: Product,
23+
resource?: InterpreterUri,
24+
): Promise<IModuleInstaller | undefined> {
25+
const channels = await this.getInstallationChannels(resource);
26+
if (channels.length === 1) {
27+
return channels[0];
28+
}
29+
30+
const productName = ProductNames.get(product)!;
31+
const appShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell);
32+
if (channels.length === 0) {
33+
await this.showNoInstallersMessage(isResource(resource) ? resource : undefined);
34+
return;
35+
}
36+
37+
const placeHolder = `Select an option to install ${productName}`;
38+
const options = channels.map((installer) => {
39+
return {
40+
label: `Install using ${installer.displayName}`,
41+
description: '',
42+
installer,
43+
};
44+
});
45+
const selection = await appShell.showQuickPick<typeof options[0]>(options, {
46+
matchOnDescription: true,
47+
matchOnDetail: true,
48+
placeHolder,
49+
});
50+
return selection ? selection.installer : undefined;
51+
}
52+
53+
public async getInstallationChannels(resource?: InterpreterUri): Promise<IModuleInstaller[]> {
54+
const installers = this.serviceContainer.getAll<IModuleInstaller>(IModuleInstaller);
55+
const supportedInstallers: IModuleInstaller[] = [];
56+
if (installers.length === 0) {
57+
return [];
58+
}
59+
// group by priority and pick supported from the highest priority
60+
installers.sort((a, b) => b.priority - a.priority);
61+
let currentPri = installers[0].priority;
62+
for (const mi of installers) {
63+
if (mi.priority !== currentPri) {
64+
if (supportedInstallers.length > 0) {
65+
break; // return highest priority supported installers
66+
}
67+
// If none supported, try next priority group
68+
currentPri = mi.priority;
69+
}
70+
if (await mi.isSupported(resource)) {
71+
supportedInstallers.push(mi);
72+
}
73+
}
74+
return supportedInstallers;
75+
}
76+
77+
public async showNoInstallersMessage(resource?: Uri): Promise<void> {
78+
const interpreters = this.serviceContainer.get<IInterpreterService>(IInterpreterService);
79+
const interpreter = await interpreters.getActiveInterpreter(resource);
80+
if (!interpreter) {
81+
return; // Handled in the Python installation check.
82+
}
83+
84+
const appShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell);
85+
const search = 'Search for help';
86+
let result: string | undefined;
87+
if (interpreter.envType === EnvironmentType.Conda) {
88+
result = await appShell.showErrorMessage(Installer.noCondaOrPipInstaller(), Installer.searchForHelp());
89+
} else {
90+
result = await appShell.showErrorMessage(Installer.noPipInstaller(), Installer.searchForHelp());
91+
}
92+
if (result === search) {
93+
const platform = this.serviceContainer.get<IPlatformService>(IPlatformService);
94+
const osName = platform.isWindows ? 'Windows' : platform.isMac ? 'MacOS' : 'Linux';
95+
appShell.openUrl(
96+
`https://www.bing.com/search?q=Install Pip ${osName} ${
97+
interpreter.envType === EnvironmentType.Conda ? 'Conda' : ''
98+
}`,
99+
);
100+
}
101+
}
102+
}

src/condaInstaller.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/* eslint-disable class-methods-use-this */
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
// Licensed under the MIT License.
4+
5+
import { inject, injectable } from 'inversify';
6+
import { ICondaService, IComponentAdapter } from '../../interpreter/contracts';
7+
import { IServiceContainer } from '../../ioc/types';
8+
import { ModuleInstallerType } from '../../pythonEnvironments/info';
9+
import { ExecutionInfo, IConfigurationService, Product } from '../types';
10+
import { isResource } from '../utils/misc';
11+
import { ModuleInstaller, translateProductToModule } from './moduleInstaller';
12+
import { InterpreterUri, ModuleInstallFlags } from './types';
13+
14+
/**
15+
* A Python module installer for a conda environment.
16+
*/
17+
@injectable()
18+
export class CondaInstaller extends ModuleInstaller {
19+
public _isCondaAvailable: boolean | undefined;
20+
21+
// Unfortunately inversify requires the number of args in constructor to be explictly
22+
// specified as more than its base class. So we need the constructor.
23+
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
24+
constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
25+
super(serviceContainer);
26+
}
27+
28+
public get name(): string {
29+
return 'Conda';
30+
}
31+
32+
public get displayName(): string {
33+
return 'Conda';
34+
}
35+
36+
public get type(): ModuleInstallerType {
37+
return ModuleInstallerType.Conda;
38+
}
39+
40+
public get priority(): number {
41+
return 0;
42+
}
43+
44+
/**
45+
* Checks whether we can use Conda as module installer for a given resource.
46+
* We need to perform two checks:
47+
* 1. Ensure we have conda.
48+
* 2. Check if the current environment is a conda environment.
49+
* @param {InterpreterUri} [resource=] Resource used to identify the workspace.
50+
* @returns {Promise<boolean>} Whether conda is supported as a module installer or not.
51+
*/
52+
public async isSupported(resource?: InterpreterUri): Promise<boolean> {
53+
if (this._isCondaAvailable === false) {
54+
return false;
55+
}
56+
const condaLocator = this.serviceContainer.get<ICondaService>(ICondaService);
57+
this._isCondaAvailable = await condaLocator.isCondaAvailable();
58+
if (!this._isCondaAvailable) {
59+
return false;
60+
}
61+
// Now we need to check if the current environment is a conda environment or not.
62+
return this.isCurrentEnvironmentACondaEnvironment(resource);
63+
}
64+
65+
/**
66+
* Return the commandline args needed to install the module.
67+
*/
68+
protected async getExecutionInfo(
69+
moduleName: string,
70+
resource?: InterpreterUri,
71+
flags: ModuleInstallFlags = 0,
72+
): Promise<ExecutionInfo> {
73+
const condaService = this.serviceContainer.get<ICondaService>(ICondaService);
74+
const condaFile = await condaService.getCondaFile();
75+
76+
const pythonPath = isResource(resource)
77+
? this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(resource).pythonPath
78+
: resource.path;
79+
const condaLocatorService = this.serviceContainer.get<IComponentAdapter>(IComponentAdapter);
80+
const info = await condaLocatorService.getCondaEnvironment(pythonPath);
81+
const args = [flags & ModuleInstallFlags.upgrade ? 'update' : 'install'];
82+
83+
// Found that using conda-forge is best at packages like tensorboard & ipykernel which seem to get updated first on conda-forge
84+
// https://github.com/microsoft/vscode-jupyter/issues/7787 & https://github.com/microsoft/vscode-python/issues/17628
85+
// Do this just for the datascience packages.
86+
if (
87+
[
88+
Product.tensorboard,
89+
Product.ipykernel,
90+
Product.pandas,
91+
Product.nbconvert,
92+
Product.jupyter,
93+
Product.notebook,
94+
]
95+
.map(translateProductToModule)
96+
.includes(moduleName)
97+
) {
98+
args.push('-c', 'conda-forge');
99+
}
100+
if (info && info.name) {
101+
// If we have the name of the conda environment, then use that.
102+
args.push('--name');
103+
args.push(info.name.toCommandArgument());
104+
} else if (info && info.path) {
105+
// Else provide the full path to the environment path.
106+
args.push('--prefix');
107+
args.push(info.path.fileToCommandArgument());
108+
}
109+
if (flags & ModuleInstallFlags.updateDependencies) {
110+
args.push('--update-deps');
111+
}
112+
if (flags & ModuleInstallFlags.reInstall) {
113+
args.push('--force-reinstall');
114+
}
115+
args.push(moduleName);
116+
args.push('-y');
117+
return {
118+
args,
119+
execPath: condaFile,
120+
};
121+
}
122+
123+
/**
124+
* Is the provided interprter a conda environment
125+
*/
126+
private async isCurrentEnvironmentACondaEnvironment(resource?: InterpreterUri): Promise<boolean> {
127+
const condaService = this.serviceContainer.get<IComponentAdapter>(IComponentAdapter);
128+
const pythonPath = isResource(resource)
129+
? this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(resource).pythonPath
130+
: resource.path;
131+
return condaService.isCondaEnvironment(pythonPath);
132+
}
133+
}

src/extensionBuildInstaller.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { inject, injectable } from 'inversify';
7+
import { Uri } from 'vscode';
8+
import { traceDecoratorError, traceLog } from '../../logging';
9+
import { IApplicationShell, ICommandManager } from '../application/types';
10+
import { Octicons, PVSC_EXTENSION_ID } from '../constants';
11+
import { IFileSystem } from '../platform/types';
12+
import { IFileDownloader } from '../types';
13+
import { ExtensionChannels } from '../utils/localize';
14+
import { IExtensionBuildInstaller } from './types';
15+
16+
export const developmentBuildUri = 'https://pvsc.blob.core.windows.net/extension-builds/ms-python-insiders.vsix';
17+
export const vsixFileExtension = '.vsix';
18+
19+
@injectable()
20+
export class StableBuildInstaller implements IExtensionBuildInstaller {
21+
constructor(
22+
@inject(ICommandManager) private readonly cmdManager: ICommandManager,
23+
@inject(IApplicationShell) private readonly appShell: IApplicationShell,
24+
) {}
25+
26+
@traceDecoratorError('Installing stable build of extension failed')
27+
public async install(): Promise<void> {
28+
traceLog(ExtensionChannels.installingStableMessage());
29+
await this.appShell.withProgressCustomIcon(Octicons.Installing, async (progress) => {
30+
progress.report({ message: ExtensionChannels.installingStableMessage() });
31+
return this.cmdManager.executeCommand('workbench.extensions.installExtension', PVSC_EXTENSION_ID, {
32+
installOnlyNewlyAddedFromExtensionPackVSIX: true,
33+
});
34+
});
35+
traceLog(ExtensionChannels.installationCompleteMessage());
36+
}
37+
}
38+
39+
@injectable()
40+
export class InsidersBuildInstaller implements IExtensionBuildInstaller {
41+
constructor(
42+
@inject(IFileDownloader) private readonly fileDownloader: IFileDownloader,
43+
@inject(IFileSystem) private readonly fs: IFileSystem,
44+
@inject(ICommandManager) private readonly cmdManager: ICommandManager,
45+
@inject(IApplicationShell) private readonly appShell: IApplicationShell,
46+
) {}
47+
48+
@traceDecoratorError('Installing insiders build of extension failed')
49+
public async install(): Promise<void> {
50+
const vsixFilePath = await this.downloadInsiders();
51+
traceLog(ExtensionChannels.installingInsidersMessage());
52+
await this.appShell.withProgressCustomIcon(Octicons.Installing, async (progress) => {
53+
progress.report({ message: ExtensionChannels.installingInsidersMessage() });
54+
return this.cmdManager.executeCommand('workbench.extensions.installExtension', Uri.file(vsixFilePath), {
55+
installOnlyNewlyAddedFromExtensionPackVSIX: true,
56+
});
57+
});
58+
traceLog(ExtensionChannels.installationCompleteMessage());
59+
await this.fs.deleteFile(vsixFilePath);
60+
}
61+
62+
@traceDecoratorError('Downloading insiders build of extension failed')
63+
public async downloadInsiders(): Promise<string> {
64+
traceLog(ExtensionChannels.startingDownloadOutputMessage());
65+
const downloadOptions = {
66+
extension: vsixFileExtension,
67+
progressMessagePrefix: ExtensionChannels.downloadingInsidersMessage(),
68+
};
69+
return this.fileDownloader.downloadFile(developmentBuildUri, downloadOptions).then((file) => {
70+
traceLog(ExtensionChannels.downloadCompletedOutputMessage());
71+
return file;
72+
});
73+
}
74+
}

0 commit comments

Comments
 (0)