Skip to content

Commit 321e204

Browse files
author
Mikhail Arkhipov
authored
Introduce per user installation of modules and elevated global install as an option (#532)
* Basic tokenizer * Fixed property names * Tests, round I * Tests, round II * tokenizer test * Remove temorary change * Fix merge issue * Merge conflict * Merge conflict * Completion test * Fix last line * Fix javascript math * Make test await for results * Add license headers * Rename definitions to types * License headers * Fix typo in completion details (typo) * Fix hover test * Russian translations * Update to better translation * Fix typo * #70 How to get all parameter info when filling in a function param list * Fix #70 How to get all parameter info when filling in a function param list * Clean up * Clean imports * CR feedback * Trim whitespace for test stability * More tests * Better handle no-parameters documentation * Better handle ellipsis and Python3 * Basic services * Install check * Output installer messages * Warn default Mac OS interpreter * Remove test change * Add tests * PR feedback * CR feedback * Mock process instead * Fix Brew detection * Update test * Elevated module install * Fix path check * Add check suppression option & suppress vor VE by default * Fix most linter tests * Merge conflict * Per-user install * Handle VE/Conda * Fix tests * Remove double service * Better mocking
1 parent 53b954f commit 321e204

File tree

10 files changed

+6432
-13
lines changed

10 files changed

+6432
-13
lines changed

package-lock.json

Lines changed: 6322 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,12 @@
925925
"description": "Whether to check if Python is installed.",
926926
"scope": "resource"
927927
},
928+
"python.globalModuleInstallation": {
929+
"type": "boolean",
930+
"default": false,
931+
"description": "Whether to install Python modules globally.",
932+
"scope": "resource"
933+
},
928934
"python.linting.enabled": {
929935
"type": "boolean",
930936
"default": true,
@@ -1547,6 +1553,7 @@
15471553
"reflect-metadata": "^0.1.10",
15481554
"rxjs": "^5.5.2",
15491555
"semver": "^5.4.1",
1556+
"sudo-prompt": "^8.0.0",
15501557
"tmp": "0.0.29",
15511558
"tree-kill": "^1.1.0",
15521559
"typescript-char": "^0.0.0",

pythonFiles/completion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ def _serialize_completions(self, script, identifier=None, prefix=''):
198198
# we pass 'text' here only for fuzzy matcher
199199
if value:
200200
_completion['snippet'] = '%s=${1:%s}$0' % (name, value)
201-
_completion['text'] = '%s=%s' % (name, value)
201+
_completion['text'] = '%s=' % (name)
202202
else:
203203
_completion['snippet'] = '%s=$1$0' % name
204204
_completion['text'] = name

src/client/common/configSettings.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface IPythonSettings {
2727
envFile: string;
2828
disablePromptForFeatures: string[];
2929
disableInstallationChecks: boolean;
30+
globalModuleInstallation: boolean;
3031
}
3132
export interface ISortImportSettings {
3233
path: string;
@@ -147,6 +148,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings {
147148
public sortImports: ISortImportSettings;
148149
public workspaceSymbols: IWorkspaceSymbolSettings;
149150
public disableInstallationChecks: boolean;
151+
public globalModuleInstallation: boolean;
150152

151153
private workspaceRoot: vscode.Uri;
152154
private disposables: vscode.Disposable[] = [];
@@ -224,7 +226,10 @@ export class PythonSettings extends EventEmitter implements IPythonSettings {
224226
} else {
225227
this.linting = lintingSettings;
226228
}
229+
227230
this.disableInstallationChecks = pythonSettings.get<boolean>('disableInstallationCheck') === true;
231+
this.globalModuleInstallation = pythonSettings.get<boolean>('globalModuleInstallation') === true;
232+
228233
// tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion
229234
const sortImportSettings = systemVariables.resolveAny(pythonSettings.get<ISortImportSettings>('sortImports'))!;
230235
if (this.sortImports) {

src/client/common/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export function isNotInstalledError(error: Error): boolean {
1313
return true;
1414
}
1515

16-
const isModuleNoInstalledError = errorObj.code === 1 && error.message.indexOf('No module named') >= 0;
16+
const isModuleNoInstalledError = error.message.indexOf('No module named') >= 0;
1717
return errorObj.code === 'ENOENT' || errorObj.code === 127 || isModuleNoInstalledError;
1818
}
1919

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,97 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
// tslint:disable-next-line:no-require-imports no-var-requires
5+
const sudo = require('sudo-prompt');
6+
7+
import * as fs from 'fs';
48
import { injectable } from 'inversify';
5-
import { Uri } from 'vscode';
9+
import * as path from 'path';
10+
import * as vscode from 'vscode';
11+
import { IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE, InterpreterType } from '../../interpreter/contracts';
612
import { IServiceContainer } from '../../ioc/types';
713
import { PythonSettings } from '../configSettings';
14+
import { STANDARD_OUTPUT_CHANNEL } from '../constants';
15+
import { IFileSystem } from '../platform/types';
816
import { ITerminalService } from '../terminal/types';
9-
import { ExecutionInfo } from '../types';
17+
import { ExecutionInfo, IOutputChannel } from '../types';
1018

1119
@injectable()
1220
export abstract class ModuleInstaller {
1321
constructor(protected serviceContainer: IServiceContainer) { }
14-
public async installModule(name: string, resource?: Uri): Promise<void> {
22+
public async installModule(name: string, resource?: vscode.Uri): Promise<void> {
1523
const executionInfo = await this.getExecutionInfo(name, resource);
1624
const terminalService = this.serviceContainer.get<ITerminalService>(ITerminalService);
1725

1826
if (executionInfo.moduleName) {
19-
const pythonPath = PythonSettings.getInstance(resource).pythonPath;
20-
await terminalService.sendCommand(pythonPath, ['-m', 'pip'].concat(executionInfo.args));
27+
const settings = PythonSettings.getInstance(resource);
28+
const args = ['-m', 'pip'].concat(executionInfo.args);
29+
const pythonPath = settings.pythonPath;
30+
31+
const locator = this.serviceContainer.get<IInterpreterLocatorService>(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE);
32+
const fileSystem = this.serviceContainer.get<IFileSystem>(IFileSystem);
33+
const interpreters = await locator.getInterpreters(resource);
34+
35+
const currentInterpreter = interpreters.length > 1
36+
? interpreters.filter(x => fileSystem.arePathsSame(x.path, pythonPath))[0]
37+
: interpreters[0];
38+
39+
if (!currentInterpreter || currentInterpreter.type !== InterpreterType.Unknown) {
40+
await terminalService.sendCommand(pythonPath, args);
41+
} else if (settings.globalModuleInstallation) {
42+
if (await this.isPathWritableAsync(path.dirname(pythonPath))) {
43+
await terminalService.sendCommand(pythonPath, args);
44+
} else {
45+
this.elevatedInstall(pythonPath, args);
46+
}
47+
} else {
48+
await terminalService.sendCommand(pythonPath, args.concat(['--user']));
49+
}
2150
} else {
2251
await terminalService.sendCommand(executionInfo.execPath!, executionInfo.args);
2352
}
2453
}
25-
public abstract isSupported(resource?: Uri): Promise<boolean>;
26-
protected abstract getExecutionInfo(moduleName: string, resource?: Uri): Promise<ExecutionInfo>;
54+
public abstract isSupported(resource?: vscode.Uri): Promise<boolean>;
55+
protected abstract getExecutionInfo(moduleName: string, resource?: vscode.Uri): Promise<ExecutionInfo>;
56+
57+
private async isPathWritableAsync(directoryPath: string): Promise<boolean> {
58+
const filePath = `${directoryPath}${path.sep}___vscpTest___`;
59+
return new Promise<boolean>(resolve => {
60+
fs.open(filePath, fs.constants.O_CREAT | fs.constants.O_RDWR, (error, fd) => {
61+
if (!error) {
62+
fs.close(fd, (e) => {
63+
fs.unlink(filePath);
64+
});
65+
}
66+
return resolve(!error);
67+
});
68+
});
69+
}
70+
71+
private elevatedInstall(execPath: string, args: string[]) {
72+
const options = {
73+
name: 'VS Code Python'
74+
};
75+
const outputChannel = this.serviceContainer.get<vscode.OutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL);
76+
const command = `"${execPath.replace(/\\/g, '/')}" ${args.join(' ')}`;
77+
78+
outputChannel.appendLine('');
79+
outputChannel.appendLine(`[Elevated] ${command}`);
80+
81+
sudo.exec(command, options, (error, stdout, stderr) => {
82+
if (error) {
83+
vscode.window.showErrorMessage(error);
84+
} else {
85+
outputChannel.show();
86+
if (stdout) {
87+
outputChannel.appendLine('');
88+
outputChannel.append(stdout);
89+
}
90+
if (stderr) {
91+
outputChannel.appendLine('');
92+
outputChannel.append(`Warning: ${stderr}`);
93+
}
94+
}
95+
});
96+
}
2797
}

src/client/common/serviceRegistry.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import { Installer } from './installer/installer';
88
import { Logger } from './logger';
99
import { PersistentStateFactory } from './persistentState';
1010
import { IS_64_BIT, IS_WINDOWS } from './platform/constants';
11+
import { FileSystem } from './platform/fileSystem';
1112
import { PathUtils } from './platform/pathUtils';
13+
import { PlatformService } from './platform/platformService';
14+
import { IFileSystem, IPlatformService } from './platform/types';
1215
import { CurrentProcess } from './process/currentProcess';
1316
import { TerminalService } from './terminal/service';
1417
import { ITerminalService } from './terminal/types';
@@ -25,4 +28,5 @@ export function registerTypes(serviceManager: IServiceManager) {
2528
serviceManager.addSingleton<IApplicationShell>(IApplicationShell, ApplicationShell);
2629
serviceManager.addSingleton<ICurrentProcess>(ICurrentProcess, CurrentProcess);
2730
serviceManager.addSingleton<IInstaller>(IInstaller, Installer);
31+
serviceManager.addSingleton<IFileSystem>(IFileSystem, FileSystem);
2832
}

src/client/common/terminal/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
export const ITerminalService = Symbol('ITerminalCommandService');
4+
export const ITerminalService = Symbol('ITerminalService');
55

66
export interface ITerminalService {
77
sendCommand(command: string, args: string[]): Promise<void>;

src/client/formatters/baseFormatter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export abstract class BaseFormatter {
8888
if (isNotInstalledError(error)) {
8989
const installer = this.serviceContainer.get<IInstaller>(IInstaller);
9090
const isInstalled = await installer.isInstalled(this.product, resource);
91-
if (isInstalled) {
91+
if (!isInstalled) {
9292
customError += `\nYou could either install the '${this.Id}' formatter, turn it off or use another formatter.`;
9393
installer.promptToInstall(this.product, resource).catch(ex => console.error('Python Extension: promptToInstall', ex));
9494
}

src/test/common/moduleInstaller.test.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ import { PipInstaller } from '../../client/common/installer/pipInstaller';
88
import { IModuleInstaller } from '../../client/common/installer/types';
99
import { Logger } from '../../client/common/logger';
1010
import { PersistentStateFactory } from '../../client/common/persistentState';
11+
import { FileSystem } from '../../client/common/platform/fileSystem';
1112
import { PathUtils } from '../../client/common/platform/pathUtils';
12-
import { Architecture } from '../../client/common/platform/types';
13+
import { PlatformService } from '../../client/common/platform/platformService';
14+
import { Architecture, IFileSystem, IPlatformService } from '../../client/common/platform/types';
1315
import { CurrentProcess } from '../../client/common/process/currentProcess';
1416
import { IProcessService, IPythonExecutionFactory } from '../../client/common/process/types';
1517
import { ITerminalService } from '../../client/common/terminal/types';
1618
import { ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, IsWindows } from '../../client/common/types';
1719
import { ICondaLocatorService, IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE, InterpreterType } from '../../client/interpreter/contracts';
20+
import { PythonInterpreterLocatorService } from '../../client/interpreter/locators/index';
1821
import { updateSetting } from '../common';
1922
import { rootWorkspaceUri } from '../common';
2023
import { MockProvider } from '../interpreters/mocks';
@@ -60,6 +63,8 @@ suite('Module Installer', () => {
6063
ioc.serviceManager.addSingleton<ICondaLocatorService>(ICondaLocatorService, MockCondaLocator);
6164
ioc.serviceManager.addSingleton<IPathUtils>(IPathUtils, PathUtils);
6265
ioc.serviceManager.addSingleton<ICurrentProcess>(ICurrentProcess, CurrentProcess);
66+
ioc.serviceManager.addSingleton<IFileSystem>(IFileSystem, FileSystem);
67+
ioc.serviceManager.addSingleton<IPlatformService>(IPlatformService, PlatformService);
6368

6469
ioc.registerMockProcessTypes();
6570
ioc.serviceManager.addSingleton<ITerminalService>(ITerminalService, MockTerminalService);
@@ -136,6 +141,9 @@ suite('Module Installer', () => {
136141
});
137142

138143
test('Validate pip install arguments', async () => {
144+
const mockInterpreterLocator = new MockProvider([{ path: await getCurrentPythonPath(), type: InterpreterType.Unknown }]);
145+
ioc.serviceManager.addSingletonInstance<IInterpreterLocatorService>(IInterpreterLocatorService, mockInterpreterLocator, INTERPRETER_LOCATOR_SERVICE);
146+
139147
const moduleName = 'xyz';
140148
const terminalService = ioc.serviceContainer.get<MockTerminalService>(ITerminalService);
141149

@@ -148,10 +156,13 @@ suite('Module Installer', () => {
148156
const commandSent = await terminalService.commandSent;
149157
const commandParts = commandSent.split(' ');
150158
commandParts.shift();
151-
expect(commandParts.join(' ')).equal(`-m pip install -U ${moduleName}`, 'Invalid command sent to terminal for installation.');
159+
expect(commandParts.join(' ')).equal(`-m pip install -U ${moduleName} --user`, 'Invalid command sent to terminal for installation.');
152160
});
153161

154162
test('Validate Conda install arguments', async () => {
163+
const mockInterpreterLocator = new MockProvider([{ path: await getCurrentPythonPath(), type: InterpreterType.Conda }]);
164+
ioc.serviceManager.addSingletonInstance<IInterpreterLocatorService>(IInterpreterLocatorService, mockInterpreterLocator, INTERPRETER_LOCATOR_SERVICE);
165+
155166
const moduleName = 'xyz';
156167
const terminalService = ioc.serviceContainer.get<MockTerminalService>(ITerminalService);
157168

0 commit comments

Comments
 (0)