diff --git a/package.json b/package.json index 0dbd5e550470..2d4f69d94ad7 100644 --- a/package.json +++ b/package.json @@ -956,6 +956,25 @@ "type": "string" } }, + "python.disablePromptForFeatures": { + "type": "array", + "default": [], + "description": "Do not display a prompt to install these features", + "items": { + "type": "string", + "default": "pylint", + "description": "Feature", + "enum": [ + "flake8", + "mypy", + "pep8", + "pylama", + "prospector", + "pydocstyle", + "pylint" + ] + } + }, "python.linting.enabled": { "type": "boolean", "default": true, diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 4514079fda33..2897b50eee1b 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -23,6 +23,7 @@ export interface IPythonSettings { sortImports: ISortImportSettings; workspaceSymbols: IWorkspaceSymbolSettings; envFile: string; + disablePromptForFeatures: string[]; } export interface ISortImportSettings { path: string; @@ -166,6 +167,8 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { this.devOptions = systemVariables.resolveAny(pythonSettings.get('devOptions'))!; this.devOptions = Array.isArray(this.devOptions) ? this.devOptions : []; let lintingSettings = systemVariables.resolveAny(pythonSettings.get('linting'))!; + this.disablePromptForFeatures = pythonSettings.get('disablePromptForFeatures')!; + this.disablePromptForFeatures = Array.isArray(this.disablePromptForFeatures) ? this.disablePromptForFeatures : []; if (this.linting) { Object.assign(this.linting, lintingSettings); } @@ -353,6 +356,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { } public jediPath: string; public envFile: string; + public disablePromptForFeatures: string[]; public venvPath: string; public devOptions: string[]; public linting: ILintingSettings; @@ -422,4 +426,4 @@ function isValidPythonPath(pythonPath: string): boolean { catch (ex) { return false; } -} \ No newline at end of file +} diff --git a/src/client/common/installer.ts b/src/client/common/installer.ts index c78b95b73d8a..1b3991047944 100644 --- a/src/client/common/installer.ts +++ b/src/client/common/installer.ts @@ -1,8 +1,8 @@ import * as vscode from 'vscode'; import * as settings from './configSettings'; -import { createDeferred, isNotInstalledError } from './helpers'; -import { execPythonFile, getFullyQualifiedPythonInterpreterPath } from './utils'; import * as os from 'os'; +import { isNotInstalledError } from './helpers'; +import { execPythonFile, getFullyQualifiedPythonInterpreterPath } from './utils'; import { Documentation } from './constants'; export enum Product { @@ -86,9 +86,6 @@ export const Linters: Product[] = [ Product.pydocstyle ]; -const Formatters: Product[] = [Product.autopep8, Product.yapf]; -const TestFrameworks: Product[] = [Product.pytest, Product.nosetest, Product.unittest]; - const ProductNames = new Map(); ProductNames.set(Product.autopep8, 'autopep8'); ProductNames.set(Product.flake8, 'flake8'); @@ -146,9 +143,9 @@ ProductTypes.set(Product.yapf, ProductType.Formatter); ProductTypes.set(Product.rope, ProductType.RefactoringLibrary); export class Installer implements vscode.Disposable { - private static terminal: vscode.Terminal; + private static terminal: vscode.Terminal | undefined | null; private disposables: vscode.Disposable[] = []; - constructor(private outputChannel: vscode.OutputChannel = null) { + constructor(private outputChannel?: vscode.OutputChannel) { this.disposables.push(vscode.window.onDidCloseTerminal(term => { if (term === Installer.terminal) { Installer.terminal = null; @@ -158,46 +155,65 @@ export class Installer implements vscode.Disposable { public dispose() { this.disposables.forEach(d => d.dispose()); } + public shouldDisplayPrompt(product: Product) { + const productName = ProductNames.get(product)!; + return settings.PythonSettings.getInstance().disablePromptForFeatures.indexOf(productName) === -1; + } - promptToInstall(product: Product): Thenable { - const productType = ProductTypes.get(product); + async promptToInstall(product: Product) { + const productType = ProductTypes.get(product)!; const productTypeName = ProductTypeNames.get(productType); - const productName = ProductNames.get(product); + const productName = ProductNames.get(product)!; + + if (!this.shouldDisplayPrompt(product)) { + const message = `${productTypeName} '${productName}' not installed.`; + if (this.outputChannel) { + this.outputChannel.appendLine(message); + } + else { + console.warn(message); + } + return; + } const installOption = 'Install ' + productName; const disableOption = 'Disable ' + productTypeName; - const disableOptionGlobally = `Disable ${productTypeName} globally`; + const dontShowAgain = `Don't show this prompt again`; const alternateFormatter = product === Product.autopep8 ? 'yapf' : 'autopep8'; const useOtherFormatter = `Use '${alternateFormatter}' formatter`; const options = []; options.push(installOption); if (productType === ProductType.Formatter) { - options.push(...[installOption, useOtherFormatter]); + options.push(...[useOtherFormatter]); } if (SettingToDisableProduct.has(product)) { - options.push(...[disableOption, disableOptionGlobally]); + options.push(...[disableOption, dontShowAgain]); } return vscode.window.showErrorMessage(`${productTypeName} ${productName} is not installed`, ...options).then(item => { switch (item) { case installOption: { return this.install(product); } - case disableOptionGlobally: case disableOption: { - const global = item === disableOptionGlobally; if (Linters.indexOf(product) >= 0) { - return disableLinter(product, global); + return disableLinter(product); } else { const pythonConfig = vscode.workspace.getConfiguration('python'); - const settingToDisable = SettingToDisableProduct.get(product); - return pythonConfig.update(settingToDisable, false, global); + const settingToDisable = SettingToDisableProduct.get(product)!; + return pythonConfig.update(settingToDisable, false); } } case useOtherFormatter: { const pythonConfig = vscode.workspace.getConfiguration('python'); return pythonConfig.update('formatting.provider', alternateFormatter); } + case dontShowAgain: { + const pythonConfig = vscode.workspace.getConfiguration('python'); + const features = pythonConfig.get('disablePromptForFeatures', [] as string[]); + features.push(productName); + return pythonConfig.update('disablePromptForFeatures', features, true); + } case 'Help': { return Promise.resolve(); } @@ -215,7 +231,7 @@ export class Installer implements vscode.Disposable { return Promise.resolve(); } - let installArgs = ProductInstallScripts.get(product); + let installArgs = ProductInstallScripts.get(product)!; let pipIndex = installArgs.indexOf('pip'); if (pipIndex > 0) { installArgs = installArgs.slice(); @@ -228,8 +244,8 @@ export class Installer implements vscode.Disposable { if (this.outputChannel && installArgs[0] === '-m') { // Errors are just displayed to the user this.outputChannel.show(); - return execPythonFile(settings.PythonSettings.getInstance().pythonPath, installArgs, vscode.workspace.rootPath, true, (data) => { - this.outputChannel.append(data); + return execPythonFile(settings.PythonSettings.getInstance().pythonPath, installArgs, vscode.workspace.rootPath!, true, (data) => { + this.outputChannel!.append(data); }); } else { @@ -249,8 +265,8 @@ export class Installer implements vscode.Disposable { installScript = `${pythonPath} ${installScript}`; } } - Installer.terminal.sendText(installScript); - Installer.terminal.show(false); + Installer.terminal!.sendText(installScript); + Installer.terminal!.show(false); }); } } @@ -266,7 +282,7 @@ export class Installer implements vscode.Disposable { export function disableLinter(product: Product, global?: boolean) { const pythonConfig = vscode.workspace.getConfiguration('python'); - const settingToDisable = SettingToDisableProduct.get(product); + const settingToDisable = SettingToDisableProduct.get(product)!; if (vscode.workspace.rootPath) { return pythonConfig.update(settingToDisable, false, global); } @@ -275,12 +291,12 @@ export function disableLinter(product: Product, global?: boolean) { } } -function isProductInstalled(product: Product): Promise { +async function isProductInstalled(product: Product): Promise { if (!ProductExecutableAndArgs.has(product)) { return; } - const prodExec = ProductExecutableAndArgs.get(product); - return execPythonFile(prodExec.executable, prodExec.args.concat(['--version']), vscode.workspace.rootPath, false) + const prodExec = ProductExecutableAndArgs.get(product)!; + return execPythonFile(prodExec.executable, prodExec.args.concat(['--version']), vscode.workspace.rootPath!, false) .then(() => { return true; }).catch(reason => { @@ -289,6 +305,6 @@ function isProductInstalled(product: Product): Promise { } function uninstallproduct(product: Product): Promise { - const uninstallArgs = ProductUninstallScripts.get(product); - return execPythonFile('python', uninstallArgs, vscode.workspace.rootPath, false); -} \ No newline at end of file + const uninstallArgs = ProductUninstallScripts.get(product)!; + return execPythonFile('python', uninstallArgs, vscode.workspace.rootPath!, false); +} diff --git a/src/client/common/systemVariables.ts b/src/client/common/systemVariables.ts index 4dc5a1082328..f0df0218c2d9 100644 --- a/src/client/common/systemVariables.ts +++ b/src/client/common/systemVariables.ts @@ -137,7 +137,7 @@ export class SystemVariables extends AbstractSystemVariables { constructor() { super(); - this._workspaceRoot = typeof vscode.workspace.rootPath === 'string' ? vscode.workspace.rootPath : __dirname;; + this._workspaceRoot = typeof vscode.workspace.rootPath === 'string' ? vscode.workspace.rootPath : __dirname; this._workspaceRootFolderName = Path.basename(this._workspaceRoot); Object.keys(process.env).forEach(key => { this[`env:${key}`] = this[`env.${key}`] = process.env[key]; @@ -155,4 +155,4 @@ export class SystemVariables extends AbstractSystemVariables { public get workspaceRootFolderName(): string { return this._workspaceRootFolderName; } -} \ No newline at end of file +} diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index abf4e6cc22fe..de114a51faf5 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -48,7 +48,7 @@ export class InterpreterDisplay implements Disposable { await Promise.all([interpreterExists, displayName, virtualEnvName]) .then(([interpreterExists, displayName, virtualEnvName]) => { const dislayNameSuffix = virtualEnvName.length > 0 ? ` (${virtualEnvName})` : ''; - this.statusBar.text = `${displayName}${dislayNameSuffix}`;; + this.statusBar.text = `${displayName}${dislayNameSuffix}`; if (!interpreterExists && displayName === defaultDisplayName && interpreters.length > 0) { this.statusBar.color = 'yellow'; diff --git a/src/client/providers/jediProxy.ts b/src/client/providers/jediProxy.ts index 890f5ba4f5d4..5b9220dfb223 100644 --- a/src/client/providers/jediProxy.ts +++ b/src/client/providers/jediProxy.ts @@ -185,7 +185,7 @@ function handleError(source: string, errorMessage: string) { logger.error(source + ' jediProxy', `Error (${source}) ${errorMessage}`); } -let spawnRetryAttempts = 0;; +let spawnRetryAttempts = 0; function spawnProcess(dir: string) { try { let environmentVariables = { 'PYTHONUNBUFFERED': '1' };