GeezerVison is showing github.com — Introduce per user installation of modules and elevated global instal… · adInfimum/vscode-python@321e204
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: package-lock.json +6,322Lines changed: 6322 additions & 0 deletions Collapse file Copy file name to clipboardExpand all lines: package.json +7Lines changed: 7 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 925 | 925 | "description": "Whether to check if Python is installed.", | 926 | 926 | "scope": "resource" | 927 | 927 | }, | | 928 | + "python.globalModuleInstallation": { | | 929 | + "type": "boolean", | | 930 | + "default": false, | | 931 | + "description": "Whether to install Python modules globally.", | | 932 | + "scope": "resource" | | 933 | + }, | 928 | 934 | "python.linting.enabled": { | 929 | 935 | "type": "boolean", | 930 | 936 | "default": true, | | 1547 | 1553 | "reflect-metadata": "^0.1.10", | 1548 | 1554 | "rxjs": "^5.5.2", | 1549 | 1555 | "semver": "^5.4.1", | | 1556 | + "sudo-prompt": "^8.0.0", | 1550 | 1557 | "tmp": "0.0.29", | 1551 | 1558 | "tree-kill": "^1.1.0", | 1552 | 1559 | "typescript-char": "^0.0.0", | |
Collapse file Copy file name to clipboardExpand all lines: pythonFiles/completion.py +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -198,7 +198,7 @@ def _serialize_completions(self, script, identifier=None, prefix=''): | 198 | 198 | # we pass 'text' here only for fuzzy matcher | 199 | 199 | if value: | 200 | 200 | _completion['snippet'] = '%s=${1:%s}$0' % (name, value) | 201 | | - _completion['text'] = '%s=%s' % (name, value) | | 201 | + _completion['text'] = '%s=' % (name) | 202 | 202 | else: | 203 | 203 | _completion['snippet'] = '%s=$1$0' % name | 204 | 204 | _completion['text'] = name | |
Collapse file Copy file name to clipboardExpand all lines: src/client/common/configSettings.ts +5Lines changed: 5 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -27,6 +27,7 @@ export interface IPythonSettings { | 27 | 27 | envFile: string; | 28 | 28 | disablePromptForFeatures: string[]; | 29 | 29 | disableInstallationChecks: boolean; | | 30 | + globalModuleInstallation: boolean; | 30 | 31 | } | 31 | 32 | export interface ISortImportSettings { | 32 | 33 | path: string; | @@ -147,6 +148,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { | 147 | 148 | public sortImports: ISortImportSettings; | 148 | 149 | public workspaceSymbols: IWorkspaceSymbolSettings; | 149 | 150 | public disableInstallationChecks: boolean; | | 151 | + public globalModuleInstallation: boolean; | 150 | 152 |
| 151 | 153 | private workspaceRoot: vscode.Uri; | 152 | 154 | private disposables: vscode.Disposable[] = []; | @@ -224,7 +226,10 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { | 224 | 226 | } else { | 225 | 227 | this.linting = lintingSettings; | 226 | 228 | } | | 229 | + | 227 | 230 | this.disableInstallationChecks = pythonSettings.get<boolean>('disableInstallationCheck') === true; | | 231 | + this.globalModuleInstallation = pythonSettings.get<boolean>('globalModuleInstallation') === true; | | 232 | + | 228 | 233 | // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion | 229 | 234 | const sortImportSettings = systemVariables.resolveAny(pythonSettings.get<ISortImportSettings>('sortImports'))!; | 230 | 235 | if (this.sortImports) { | |
Collapse file Copy file name to clipboardExpand all lines: src/client/common/helpers.ts +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -13,7 +13,7 @@ export function isNotInstalledError(error: Error): boolean { | 13 | 13 | return true; | 14 | 14 | } | 15 | 15 |
| 16 | | - const isModuleNoInstalledError = errorObj.code === 1 && error.message.indexOf('No module named') >= 0; | | 16 | + const isModuleNoInstalledError = error.message.indexOf('No module named') >= 0; | 17 | 17 | return errorObj.code === 'ENOENT' || errorObj.code === 127 || isModuleNoInstalledError; | 18 | 18 | } | 19 | 19 |
| |
Collapse file Copy file name to clipboard +77-7Lines changed: 77 additions & 7 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | // Copyright (c) Microsoft Corporation. All rights reserved. | 2 | 2 | // Licensed under the MIT License. | 3 | 3 |
| | 4 | +// tslint:disable-next-line:no-require-imports no-var-requires | | 5 | +const sudo = require('sudo-prompt'); | | 6 | + | | 7 | +import * as fs from 'fs'; | 4 | 8 | 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'; | 6 | 12 | import { IServiceContainer } from '../../ioc/types'; | 7 | 13 | import { PythonSettings } from '../configSettings'; | | 14 | +import { STANDARD_OUTPUT_CHANNEL } from '../constants'; | | 15 | +import { IFileSystem } from '../platform/types'; | 8 | 16 | import { ITerminalService } from '../terminal/types'; | 9 | | -import { ExecutionInfo } from '../types'; | | 17 | +import { ExecutionInfo, IOutputChannel } from '../types'; | 10 | 18 |
| 11 | 19 | @injectable() | 12 | 20 | export abstract class ModuleInstaller { | 13 | 21 | 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> { | 15 | 23 | const executionInfo = await this.getExecutionInfo(name, resource); | 16 | 24 | const terminalService = this.serviceContainer.get<ITerminalService>(ITerminalService); | 17 | 25 |
| 18 | 26 | 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 | + } | 21 | 50 | } else { | 22 | 51 | await terminalService.sendCommand(executionInfo.execPath!, executionInfo.args); | 23 | 52 | } | 24 | 53 | } | 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 | + } | 27 | 97 | } |
Collapse file Copy file name to clipboardExpand all lines: src/client/common/serviceRegistry.ts +4Lines changed: 4 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -8,7 +8,10 @@ import { Installer } from './installer/installer'; | 8 | 8 | import { Logger } from './logger'; | 9 | 9 | import { PersistentStateFactory } from './persistentState'; | 10 | 10 | import { IS_64_BIT, IS_WINDOWS } from './platform/constants'; | | 11 | +import { FileSystem } from './platform/fileSystem'; | 11 | 12 | import { PathUtils } from './platform/pathUtils'; | | 13 | +import { PlatformService } from './platform/platformService'; | | 14 | +import { IFileSystem, IPlatformService } from './platform/types'; | 12 | 15 | import { CurrentProcess } from './process/currentProcess'; | 13 | 16 | import { TerminalService } from './terminal/service'; | 14 | 17 | import { ITerminalService } from './terminal/types'; | @@ -25,4 +28,5 @@ export function registerTypes(serviceManager: IServiceManager) { | 25 | 28 | serviceManager.addSingleton<IApplicationShell>(IApplicationShell, ApplicationShell); | 26 | 29 | serviceManager.addSingleton<ICurrentProcess>(ICurrentProcess, CurrentProcess); | 27 | 30 | serviceManager.addSingleton<IInstaller>(IInstaller, Installer); | | 31 | + serviceManager.addSingleton<IFileSystem>(IFileSystem, FileSystem); | 28 | 32 | } |
Collapse file Copy file name to clipboardExpand all lines: src/client/common/terminal/types.ts +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | // Copyright (c) Microsoft Corporation. All rights reserved. | 2 | 2 | // Licensed under the MIT License. | 3 | 3 |
| 4 | | -export const ITerminalService = Symbol('ITerminalCommandService'); | | 4 | +export const ITerminalService = Symbol('ITerminalService'); | 5 | 5 |
| 6 | 6 | export interface ITerminalService { | 7 | 7 | sendCommand(command: string, args: string[]): Promise<void>; | |
Collapse file Copy file name to clipboardExpand all lines: src/client/formatters/baseFormatter.ts +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -88,7 +88,7 @@ export abstract class BaseFormatter { | 88 | 88 | if (isNotInstalledError(error)) { | 89 | 89 | const installer = this.serviceContainer.get<IInstaller>(IInstaller); | 90 | 90 | const isInstalled = await installer.isInstalled(this.product, resource); | 91 | | - if (isInstalled) { | | 91 | + if (!isInstalled) { | 92 | 92 | customError += `\nYou could either install the '${this.Id}' formatter, turn it off or use another formatter.`; | 93 | 93 | installer.promptToInstall(this.product, resource).catch(ex => console.error('Python Extension: promptToInstall', ex)); | 94 | 94 | } | |
Collapse file Copy file name to clipboardExpand all lines: src/test/common/moduleInstaller.test.ts +13-2Lines changed: 13 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -8,13 +8,16 @@ import { PipInstaller } from '../../client/common/installer/pipInstaller'; | 8 | 8 | import { IModuleInstaller } from '../../client/common/installer/types'; | 9 | 9 | import { Logger } from '../../client/common/logger'; | 10 | 10 | import { PersistentStateFactory } from '../../client/common/persistentState'; | | 11 | +import { FileSystem } from '../../client/common/platform/fileSystem'; | 11 | 12 | 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'; | 13 | 15 | import { CurrentProcess } from '../../client/common/process/currentProcess'; | 14 | 16 | import { IProcessService, IPythonExecutionFactory } from '../../client/common/process/types'; | 15 | 17 | import { ITerminalService } from '../../client/common/terminal/types'; | 16 | 18 | import { ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, IsWindows } from '../../client/common/types'; | 17 | 19 | import { ICondaLocatorService, IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE, InterpreterType } from '../../client/interpreter/contracts'; | | 20 | +import { PythonInterpreterLocatorService } from '../../client/interpreter/locators/index'; | 18 | 21 | import { updateSetting } from '../common'; | 19 | 22 | import { rootWorkspaceUri } from '../common'; | 20 | 23 | import { MockProvider } from '../interpreters/mocks'; | @@ -60,6 +63,8 @@ suite('Module Installer', () => { | 60 | 63 | ioc.serviceManager.addSingleton<ICondaLocatorService>(ICondaLocatorService, MockCondaLocator); | 61 | 64 | ioc.serviceManager.addSingleton<IPathUtils>(IPathUtils, PathUtils); | 62 | 65 | ioc.serviceManager.addSingleton<ICurrentProcess>(ICurrentProcess, CurrentProcess); | | 66 | + ioc.serviceManager.addSingleton<IFileSystem>(IFileSystem, FileSystem); | | 67 | + ioc.serviceManager.addSingleton<IPlatformService>(IPlatformService, PlatformService); | 63 | 68 |
| 64 | 69 | ioc.registerMockProcessTypes(); | 65 | 70 | ioc.serviceManager.addSingleton<ITerminalService>(ITerminalService, MockTerminalService); | @@ -136,6 +141,9 @@ suite('Module Installer', () => { | 136 | 141 | }); | 137 | 142 |
| 138 | 143 | 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 | + | 139 | 147 | const moduleName = 'xyz'; | 140 | 148 | const terminalService = ioc.serviceContainer.get<MockTerminalService>(ITerminalService); | 141 | 149 |
| @@ -148,10 +156,13 @@ suite('Module Installer', () => { | 148 | 156 | const commandSent = await terminalService.commandSent; | 149 | 157 | const commandParts = commandSent.split(' '); | 150 | 158 | 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.'); | 152 | 160 | }); | 153 | 161 |
| 154 | 162 | 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 | + | 155 | 166 | const moduleName = 'xyz'; | 156 | 167 | const terminalService = ioc.serviceContainer.get<MockTerminalService>(ITerminalService); | 157 | 168 |
| |
|