Skip to content

Commit

Permalink
Implements linting configuration (#599)
Browse files Browse the repository at this point in the history
* 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

* #385 Auto-Indentation doesn't work after comment

* #141 Auto indentation broken when return keyword involved

* Undo changes

* Round I

* Round 2

* Round 3

* Round 4

* Round 5

* no message

* Round 6

* Round 7

* Clean up targets and messages

* Settings propagation

* Tests

* Test warning

* Fix installer tests

* Tests

* Test fixes

* Fix terminal service and tests async/await

* Fix mock setup

* Test fix

* Test async/await fix

* Test fix + activate tslint on awaits

* Use command manager

* Work around updateSettings

* Multiroot fixes, partial

* More workarounds

* Multiroot tests

* Fix installer test

* Test fixes

* Disable prospector

* Enable dispose in all cases

* Fix event firing

* Min pylint options

* Min checkers & pylintrc discovery

* Fix Windows path in tests for Travis

* Fix Mac test

* Test fix
  • Loading branch information
Mikhail Arkhipov authored Jan 24, 2018
1 parent 0253995 commit df39217
Show file tree
Hide file tree
Showing 57 changed files with 8,007 additions and 849 deletions.
6,322 changes: 6,322 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

26 changes: 19 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@
"onCommand:python.buildWorkspaceSymbols",
"onCommand:python.updateSparkLibrary",
"onCommand:python.startREPL",
"onCommand:python.goToPythonObject"
"onCommand:python.goToPythonObject",
"onCommand:python.setLinter",
"onCommand:python.enableLinting"
],
"main": "./out/client/extension",
"contributes": {
Expand Down Expand Up @@ -213,6 +215,16 @@
"command": "python.goToPythonObject",
"title": "%python.command.python.goToPythonObject.title%",
"category": "Python"
},
{
"command": "python.setLinter",
"title": "%python.command.python.setLinter.title%",
"category": "Python"
},
{
"command": "python.enableLinting",
"title": "%python.command.python.enableLinting.title%",
"category": "Python"
}
],
"menus": {
Expand Down Expand Up @@ -949,12 +961,6 @@
"description": "Whether to lint Python files.",
"scope": "resource"
},
"python.linting.enabledWithoutWorkspace": {
"type": "boolean",
"default": true,
"description": "Whether to lint Python files when no workspace is opened.",
"scope": "resource"
},
"python.linting.prospectorEnabled": {
"type": "boolean",
"default": false,
Expand Down Expand Up @@ -1003,6 +1009,12 @@
"description": "Controls the maximum number of problems produced by the server.",
"scope": "resource"
},
"python.linting.pylintUseMinimalCheckers": {
"type": "boolean",
"default": true,
"description": "Whether to run Pylint with minimal set of rules.",
"scope": "resource"
},
"python.linting.pylintCategorySeverity.convention": {
"type": "string",
"default": "Information",
Expand Down
2 changes: 2 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"python.command.jupyter.gotToPreviousCell.title": "Go to Previous Cell",
"python.command.jupyter.gotToNextCell.title": "Go to Next Cell",
"python.command.python.goToPythonObject.title": "Go to Python Object",
"python.command.python.setLinter.title": "Select Linter",
"python.command.python.enableLinting.title": "Enable Linting",
"python.snippet.launch.standard.label": "Python",
"python.snippet.launch.standard.description": "Debug a Python program with standard output",
"python.snippet.launch.pyspark.label": "Python: PySpark",
Expand Down
2 changes: 2 additions & 0 deletions package.nls.ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"python.command.jupyter.gotToPreviousCell.title": "Перейти к предыдущей ячейке",
"python.command.jupyter.gotToNextCell.title": "Перейти к следующей ячейке",
"python.command.python.goToPythonObject.title": "Перейти к объекту Python",
"python.command.python.setLinter.title": "Выбрать анализатор кода",
"python.command.python.enableLinting.title": "Включить анализатор кода",
"python.snippet.launch.standard.label": "Python",
"python.snippet.launch.standard.description": "Отладить программу Python со стандартным выводом",
"python.snippet.launch.pyspark.label": "Python: PySpark",
Expand Down
26 changes: 18 additions & 8 deletions src/client/common/configSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as child_process from 'child_process';
import { EventEmitter } from 'events';
import * as path from 'path';
import * as vscode from 'vscode';
import { Uri } from 'vscode';
import { ConfigurationTarget, Uri } from 'vscode';
import {
IAutoCompeteSettings,
IFormattingSettings,
Expand Down Expand Up @@ -61,19 +61,29 @@ export class PythonSettings extends EventEmitter implements IPythonSettings {
}
// tslint:disable-next-line:function-name
public static getInstance(resource?: Uri): PythonSettings {
const workspaceFolder = resource ? vscode.workspace.getWorkspaceFolder(resource) : undefined;
let workspaceFolderUri: Uri | undefined = workspaceFolder ? workspaceFolder.uri : undefined;
if (!workspaceFolderUri && Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 0) {
workspaceFolderUri = vscode.workspace.workspaceFolders[0].uri;
}
const workspaceFolderUri = PythonSettings.getSettingsUriAndTarget(resource).uri;
const workspaceFolderKey = workspaceFolderUri ? workspaceFolderUri.fsPath : '';

if (!PythonSettings.pythonSettings.has(workspaceFolderKey)) {
const settings = new PythonSettings(workspaceFolderUri);
PythonSettings.pythonSettings.set(workspaceFolderKey, settings);
}
// tslint:disable-next-line:no-non-null-assertion
return PythonSettings.pythonSettings.get(workspaceFolderKey)!;
}

public static getSettingsUriAndTarget(resource?: Uri): { uri: Uri | undefined, target: ConfigurationTarget } {
const workspaceFolder = resource ? vscode.workspace.getWorkspaceFolder(resource) : undefined;
let workspaceFolderUri: Uri | undefined = workspaceFolder ? workspaceFolder.uri : undefined;

if (!workspaceFolderUri && Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 0) {
workspaceFolderUri = vscode.workspace.workspaceFolders[0].uri;
}

const target = workspaceFolderUri ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Global;
return { uri: workspaceFolderUri, target };
}

// tslint:disable-next-line:function-name
public static dispose() {
if (!isTestExecution()) {
Expand Down Expand Up @@ -138,7 +148,6 @@ export class PythonSettings extends EventEmitter implements IPythonSettings {
// Support for travis.
this.linting = this.linting ? this.linting : {
enabled: false,
enabledWithoutWorkspace: false,
ignorePatterns: [],
flake8Args: [], flake8Enabled: false, flake8Path: 'flake',
lintOnSave: false, maxNumberOfProblems: 100,
Expand Down Expand Up @@ -167,7 +176,8 @@ export class PythonSettings extends EventEmitter implements IPythonSettings {
mypyCategorySeverity: {
error: vscode.DiagnosticSeverity.Error,
note: vscode.DiagnosticSeverity.Hint
}
},
pylintUseMinimalCheckers: false
};
this.linting.pylintPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylintPath), workspaceRoot);
this.linting.flake8Path = getAbsolutePath(systemVariables.resolveAny(this.linting.flake8Path), workspaceRoot);
Expand Down
46 changes: 45 additions & 1 deletion src/client/common/configuration/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT License.

import { injectable } from 'inversify';
import { Uri } from 'vscode';
import { ConfigurationTarget, Uri, workspace, WorkspaceConfiguration } from 'vscode';
import { PythonSettings } from '../configSettings';
import { IConfigurationService, IPythonSettings } from '../types';

Expand All @@ -11,4 +11,48 @@ export class ConfigurationService implements IConfigurationService {
public getSettings(resource?: Uri): IPythonSettings {
return PythonSettings.getInstance(resource);
}
public async updateSettingAsync(setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise<void> {
const settingsInfo = PythonSettings.getSettingsUriAndTarget(resource);

const pythonConfig = workspace.getConfiguration('python', settingsInfo.uri);
const currentValue = pythonConfig.inspect(setting);

if (currentValue !== undefined &&
((settingsInfo.target === ConfigurationTarget.Global && currentValue.globalValue === value) ||
(settingsInfo.target === ConfigurationTarget.Workspace && currentValue.workspaceValue === value) ||
(settingsInfo.target === ConfigurationTarget.WorkspaceFolder && currentValue.workspaceFolderValue === value))) {
return;
}

await pythonConfig.update(setting, value, settingsInfo.target);
await this.verifySetting(pythonConfig, settingsInfo.target, setting, value);
}

public isTestExecution(): boolean {
return process.env.VSC_PYTHON_CI_TEST === '1';
}

private async verifySetting(pythonConfig: WorkspaceConfiguration, target: ConfigurationTarget, settingName: string, value?: {}): Promise<void> {
if (this.isTestExecution()) {
let retries = 0;
do {
const setting = pythonConfig.inspect(settingName);
if (!setting && value === undefined) {
break; // Both are unset
}
if (setting && value !== undefined) {
// Both specified
const actual = target === ConfigurationTarget.Global
? setting.globalValue
: target === ConfigurationTarget.Workspace ? setting.workspaceValue : setting.workspaceFolderValue;
if (actual === value) {
break;
}
}
// Wait for settings to get refreshed.
await new Promise((resolve, reject) => setTimeout(resolve, 250));
retries += 1;
} while (retries < 20);
}
}
}
3 changes: 3 additions & 0 deletions src/client/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const PythonLanguage = { language: 'python' };

export namespace Commands {
export const Set_Interpreter = 'python.setInterpreter';
export const Set_ShebangInterpreter = 'python.setShebangInterpreter';
export const Exec_In_Terminal = 'python.execInTerminal';
export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal';
export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell';
Expand All @@ -27,6 +28,8 @@ export namespace Commands {
export const Update_SparkLibrary = 'python.updateSparkLibrary';
export const Build_Workspace_Symbols = 'python.buildWorkspaceSymbols';
export const Start_REPL = 'python.startREPL';
export const Set_Linter = 'python.setLinter';
export const Enable_Linter = 'python.enableLinting';
}
export namespace Octicons {
export const Test_Pass = '$(check)';
Expand Down
2 changes: 1 addition & 1 deletion src/client/common/errors/moduleNotInstalledError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@

export class ModuleNotInstalledError extends Error {
constructor(moduleName: string) {
super(`Module '${moduleName} not installed.`);
super(`Module '${moduleName}' not installed.`);
}
}
61 changes: 3 additions & 58 deletions src/client/common/installer/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ConfigurationTarget, QuickPickItem, Uri, window, workspace } from 'vsco
import * as vscode from 'vscode';
import { IFormatterHelper } from '../../formatters/types';
import { IServiceContainer } from '../../ioc/types';
import { ILinterHelper } from '../../linters/types';
import { ILinterManager } from '../../linters/types';
import { ITestsHelper } from '../../unittests/common/types';
import { PythonSettings } from '../configSettings';
import { STANDARD_OUTPUT_CHANNEL } from '../constants';
Expand Down Expand Up @@ -34,17 +34,6 @@ ProductNames.set(Product.pytest, 'pytest');
ProductNames.set(Product.yapf, 'yapf');
ProductNames.set(Product.rope, 'rope');

export const SettingToDisableProduct = new Map<Product, string>();
SettingToDisableProduct.set(Product.flake8, 'linting.flake8Enabled');
SettingToDisableProduct.set(Product.mypy, 'linting.mypyEnabled');
SettingToDisableProduct.set(Product.nosetest, 'unitTest.nosetestsEnabled');
SettingToDisableProduct.set(Product.pep8, 'linting.pep8Enabled');
SettingToDisableProduct.set(Product.pylama, 'linting.pylamaEnabled');
SettingToDisableProduct.set(Product.prospector, 'linting.prospectorEnabled');
SettingToDisableProduct.set(Product.pydocstyle, 'linting.pydocstyleEnabled');
SettingToDisableProduct.set(Product.pylint, 'linting.pylintEnabled');
SettingToDisableProduct.set(Product.pytest, 'unitTest.pyTestEnabled');

// tslint:disable-next-line:variable-name
const ProductInstallationPrompt = new Map<Product, string>();
ProductInstallationPrompt.set(Product.ctags, 'Install CTags to enable Python workspace symbols');
Expand Down Expand Up @@ -92,25 +81,14 @@ export class Installer implements IInstaller {
const productTypeName = ProductTypeNames.get(productType)!;
const productName = ProductNames.get(product)!;

if (!this.shouldDisplayPrompt(product)) {
const message = `${productTypeName} '${productName}' not installed.`;
this.outputChannel.appendLine(message);
return InstallerResponse.Ignore;
}

const installOption = ProductInstallationPrompt.has(product) ? ProductInstallationPrompt.get(product)! : `Install ${productName}`;
const disableOption = `Disable ${productTypeName}`;
const dontShowAgain = 'Don\'t show this prompt again';
const alternateFormatter = product === Product.autopep8 ? 'yapf' : 'autopep8';
const useOtherFormatter = `Use '${alternateFormatter}' formatter`;
const options: string[] = [];
options.push(installOption);
if (productType === ProductType.Formatter) {
options.push(...[useOtherFormatter]);
}
if (SettingToDisableProduct.has(product)) {
options.push(...[disableOption, dontShowAgain]);
}
const item = await window.showErrorMessage(`${productTypeName} ${productName} is not installed`, ...options);
if (!item) {
return InstallerResponse.Ignore;
Expand All @@ -119,24 +97,10 @@ export class Installer implements IInstaller {
case installOption: {
return this.install(product, resource);
}
case disableOption: {
if (ProductTypes.has(product) && ProductTypes.get(product)! === ProductType.Linter) {
return this.disableLinter(product, resource).then(() => InstallerResponse.Disabled);
} else {
const settingToDisable = SettingToDisableProduct.get(product)!;
return this.updateSetting(settingToDisable, false, resource).then(() => InstallerResponse.Disabled);
}
}
case useOtherFormatter: {
return this.updateSetting('formatting.provider', alternateFormatter, resource)
.then(() => InstallerResponse.Installed);
}
case dontShowAgain: {
const pythonConfig = workspace.getConfiguration('python');
const features = pythonConfig.get('disablePromptForFeatures', [] as string[]);
features.push(productName);
return pythonConfig.update('disablePromptForFeatures', features, true).then(() => InstallerResponse.Ignore);
}
default: {
throw new Error('Invalid selection');
}
Expand Down Expand Up @@ -210,24 +174,6 @@ export class Installer implements IInstaller {
.catch(() => false);
}
}
public async disableLinter(product: Product, resource?: Uri) {
if (resource && workspace.getWorkspaceFolder(resource)) {
const settingToDisable = SettingToDisableProduct.get(product)!;
const pythonConfig = workspace.getConfiguration('python', resource);
const isMultiroot = Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 1;
const configTarget = isMultiroot ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace;
return pythonConfig.update(settingToDisable, false, configTarget);
} else {
const pythonConfig = workspace.getConfiguration('python');
return pythonConfig.update('linting.enabledWithoutWorkspace', false, true);
}
}
private shouldDisplayPrompt(product: Product) {
const productName = ProductNames.get(product)!;
const pythonConfig = workspace.getConfiguration('python');
const disablePromptForFeatures = pythonConfig.get('disablePromptForFeatures', [] as string[]);
return disablePromptForFeatures.indexOf(productName) === -1;
}
private installCTags() {
if (this.serviceContainer.get<IPlatformService>(IPlatformService).isWindows) {
this.outputChannel.appendLine('Install Universal Ctags Win32 to enable support for Workspace Symbols');
Expand Down Expand Up @@ -302,9 +248,8 @@ export class Installer implements IInstaller {
}
case ProductType.RefactoringLibrary: return this.translateProductToModuleName(product, ModuleNamePurpose.run);
case ProductType.Linter: {
const linterHelper = this.serviceContainer.get<ILinterHelper>(ILinterHelper);
const settingsPropNames = linterHelper.getSettingsPropertyNames(product);
return settings.linting[settingsPropNames.pathName] as string;
const linterManager = this.serviceContainer.get<ILinterManager>(ILinterManager);
return linterManager.getLinterInfo(product).pathName(resource);
}
default: {
throw new Error(`Unrecognized Product '${product}'`);
Expand Down
8 changes: 2 additions & 6 deletions src/client/common/installer/pipInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,14 @@ import { IModuleInstaller } from './types';

@injectable()
export class PipInstaller extends ModuleInstaller implements IModuleInstaller {
private isCondaAvailable: boolean | undefined;
public get displayName() {
return 'Pip';
}
constructor( @inject(IServiceContainer) serviceContainer: IServiceContainer) {
super(serviceContainer);
}
public isSupported(resource?: Uri): Promise<boolean> {
const pythonExecutionFactory = this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory);
return pythonExecutionFactory.create(resource)
.then(proc => proc.isModuleInstalled('pip'))
.catch(() => false);
return this.isPipAvailable(resource);
}
protected async getExecutionInfo(moduleName: string, resource?: Uri): Promise<ExecutionInfo> {
const proxyArgs = [];
Expand All @@ -37,7 +33,7 @@ export class PipInstaller extends ModuleInstaller implements IModuleInstaller {
moduleName: 'pip'
};
}
private isPipAvailable(resource?: Uri) {
private isPipAvailable(resource?: Uri): Promise<boolean> {
const pythonExecutionFactory = this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory);
return pythonExecutionFactory.create(resource)
.then(proc => proc.isModuleInstalled('pip'))
Expand Down
Loading

0 comments on commit df39217

Please sign in to comment.