From 69163179a830d174886671e8269dfe94538436fd Mon Sep 17 00:00:00 2001 From: Gram Date: Mon, 30 Dec 2019 13:36:18 +0100 Subject: [PATCH 1/5] +exceptions support --- flakehell/_logic/__init__.py | 4 ++-- flakehell/_logic/_config.py | 13 +++++++----- flakehell/_logic/_plugin.py | 25 ++++++++++++++++++++++- flakehell/_patched/_checkers.py | 35 ++++++++++++++++++++++----------- 4 files changed, 57 insertions(+), 20 deletions(-) diff --git a/flakehell/_logic/__init__.py b/flakehell/_logic/__init__.py index 96f0407..6fd0b77 100644 --- a/flakehell/_logic/__init__.py +++ b/flakehell/_logic/__init__.py @@ -3,7 +3,7 @@ from ._config import read_config from ._discover import get_installed from ._extractors import extract -from ._plugin import get_plugin_name, get_plugin_rules, check_include +from ._plugin import get_plugin_name, get_plugin_rules, check_include, get_exceptions from ._snapshot import Snapshot, prepare_cache @@ -13,6 +13,6 @@ 'color_code', 'color_description', 'get_installed', 'extract', - 'get_plugin_name', 'get_plugin_rules', 'check_include', + 'get_plugin_name', 'get_plugin_rules', 'check_include', 'get_exceptions', 'Snapshot', 'prepare_cache', ] diff --git a/flakehell/_logic/_config.py b/flakehell/_logic/_config.py index 36a4a38..fa2246a 100644 --- a/flakehell/_logic/_config.py +++ b/flakehell/_logic/_config.py @@ -34,9 +34,10 @@ def _merge_configs(*configs): for subconfig in configs: config.update(subconfig) - config['plugins'] = dict() - for subconfig in configs: - config['plugins'].update(subconfig.get('plugins', {})) + for section in ('plugins', 'exceptions'): + config[section] = dict() + for subconfig in configs: + config[section].update(subconfig.get(section, {})) return config @@ -44,8 +45,10 @@ def _merge_configs(*configs): def _parse_config(content: str): config = toml.loads(content).get('tool', {}).get('flakehell', {}) config = dict(config) - if 'plugins' in config: - config['plugins'] = dict(config['plugins']) + + for section in ('plugins', 'exceptions'): + if section in config: + config[section] = dict(config[section]) if 'base' in config: paths = config['base'] diff --git a/flakehell/_logic/_plugin.py b/flakehell/_logic/_plugin.py index cc16991..fe17c2d 100644 --- a/flakehell/_logic/_plugin.py +++ b/flakehell/_logic/_plugin.py @@ -1,5 +1,6 @@ import re -from typing import Dict, Any, List +from pathlib import Path +from typing import Dict, Any, List, Union from flake8.utils import fnmatch @@ -94,3 +95,25 @@ def check_include(code: str, rules: List[str]) -> bool: if fnmatch(code, patterns=[rule[1:]]): include = rule[0] == '+' return include + + +def get_exceptions(path: Union[str, Path], exceptions: Dict[str, List[str]], + root: Path = None) -> Dict[str, List[str]]: + if isinstance(path, str): + path = Path(path) + if root is None: + root = Path() + path.resolve() + path = path.relative_to(root) + path = path.as_posix() + + exceptions = sorted( + exceptions.items(), + key=lambda item: len(item[0]), + reverse=True, + ) + for path_rule, rules in exceptions: + if path.startswith(path_rule): + return rules + + return [] diff --git a/flakehell/_patched/_checkers.py b/flakehell/_patched/_checkers.py index 7cf5b26..1f3fe6f 100644 --- a/flakehell/_patched/_checkers.py +++ b/flakehell/_patched/_checkers.py @@ -1,11 +1,11 @@ -from typing import List, Tuple, Optional +from typing import Any, Dict, List, Tuple, Optional from flake8.checker import Manager, FileChecker from flake8.utils import fnmatch, filenames_from from .._logic import ( get_plugin_name, get_plugin_rules, check_include, make_baseline, - Snapshot, prepare_cache, + Snapshot, prepare_cache, get_exceptions, ) @@ -62,12 +62,8 @@ def make_checkers(self, paths: List[str] = None) -> None: def _make_checker(self, argument, filename, check_type, check) -> Optional['FlakeHellFileChecker']: # do not run plugins without rules specified - plugin_name = get_plugin_name(check) - rules = get_plugin_rules( - plugin_name=plugin_name, - plugins=self.options.plugins, - ) - if not rules or rules == ['-*']: + rules = self._get_rules(check=check, filename=filename) + if not rules or set(rules) == {'-*'}: return None if not self._should_create_file_checker(filename=filename, argument=argument): @@ -84,6 +80,24 @@ def _make_checker(self, argument, filename, check_type, # return None return checker + def _get_rules(self, check: Dict[str, Any], filename: str): + plugin_name = get_plugin_name(check) + rules = get_plugin_rules( + plugin_name=plugin_name, + plugins=self.options.plugins, + ) + exceptions = get_exceptions( + path=filename, + exceptions=self.options.exceptions, + ) + if exceptions: + rules = rules.copy() + rules += get_plugin_rules( + plugin_name=plugin_name, + plugins=exceptions, + ) + return rules + def _should_create_file_checker(self, filename: str, argument) -> bool: """Filter out excluded files """ @@ -133,10 +147,7 @@ def report(self) -> Tuple[int, int]: def _handle_results(self, filename: str, results: list, check: dict) -> int: plugin_name = get_plugin_name(check) - rules = get_plugin_rules( - plugin_name=plugin_name, - plugins=self.options.plugins, - ) + rules = self._get_rules(check=check, filename=filename) reported_results_count = 0 for (error_code, line_number, column, text, physical_line) in results: if self.baseline: From d6303ec572d8db0b6b071c0ffacef95256f4ac9b Mon Sep 17 00:00:00 2001 From: Gram Date: Tue, 19 May 2020 12:47:31 +0200 Subject: [PATCH 2/5] a bit more annotations --- flakehell/_logic/_config.py | 8 ++++---- flakehell/_logic/_plugin.py | 12 +++++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/flakehell/_logic/_config.py b/flakehell/_logic/_config.py index 6dcbf6e..970b5af 100644 --- a/flakehell/_logic/_config.py +++ b/flakehell/_logic/_config.py @@ -20,18 +20,18 @@ def read_config(*paths) -> Dict[str, Any]: return config -def _read_local(path: Path): +def _read_local(path: Path) -> Dict[str, Any]: with path.open('r') as stream: return _parse_config(stream.read()) -def _read_remote(url: str): +def _read_remote(url: str) -> Dict[str, Any]: http = urllib3.PoolManager() response = http.request('GET', url) return _parse_config(response.data.decode()) -def _merge_configs(*configs): +def _merge_configs(*configs) -> Dict[str, Any]: config = dict() for subconfig in configs: config.update(subconfig) @@ -44,7 +44,7 @@ def _merge_configs(*configs): return config -def _parse_config(content: str): +def _parse_config(content: str) -> Dict[str, Any]: config = toml.loads(content).get('tool', {}).get('flakehell', {}) config = dict(config) diff --git a/flakehell/_logic/_plugin.py b/flakehell/_logic/_plugin.py index fe17c2d..650f526 100644 --- a/flakehell/_logic/_plugin.py +++ b/flakehell/_logic/_plugin.py @@ -12,6 +12,7 @@ 'naming': 'pep8-naming', 'logging-format': 'flake8-logging-format', } +PluginsType = Dict[str, List[str]] def get_plugin_name(plugin: Dict[str, Any]) -> str: @@ -42,7 +43,7 @@ def get_plugin_name(plugin: Dict[str, Any]) -> str: return names[0] -def get_plugin_rules(plugin_name: str, plugins: Dict[str, List[str]]) -> List[str]: +def get_plugin_rules(plugin_name: str, plugins: PluginsType) -> List[str]: """Get rules for plugin from `plugins` in the config Plugin name can be specified as a glob expression. @@ -97,8 +98,9 @@ def check_include(code: str, rules: List[str]) -> bool: return include -def get_exceptions(path: Union[str, Path], exceptions: Dict[str, List[str]], - root: Path = None) -> Dict[str, List[str]]: +def get_exceptions( + path: Union[str, Path], exceptions: Dict[str, PluginsType], root: Path = None, +) -> PluginsType: if isinstance(path, str): path = Path(path) if root is None: @@ -112,8 +114,8 @@ def get_exceptions(path: Union[str, Path], exceptions: Dict[str, List[str]], key=lambda item: len(item[0]), reverse=True, ) - for path_rule, rules in exceptions: + for path_rule, rules in exceptions.items(): if path.startswith(path_rule): return rules - return [] + return dict() From 1bc928c9db74f7cf59c88445c196f1ae7e4dd39c Mon Sep 17 00:00:00 2001 From: Gram Date: Tue, 19 May 2020 12:48:18 +0200 Subject: [PATCH 3/5] mv --- tests/test_logic/__init__.py | 0 tests/{test_logic_extractors.py => test_logic/test_extractors.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/test_logic/__init__.py rename tests/{test_logic_extractors.py => test_logic/test_extractors.py} (100%) diff --git a/tests/test_logic/__init__.py b/tests/test_logic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_logic_extractors.py b/tests/test_logic/test_extractors.py similarity index 100% rename from tests/test_logic_extractors.py rename to tests/test_logic/test_extractors.py From e9cdd9e35e91890b8317640ab3ff6a0465dd553b Mon Sep 17 00:00:00 2001 From: Gram Date: Tue, 19 May 2020 17:01:19 +0200 Subject: [PATCH 4/5] test --- flakehell/_logic/_plugin.py | 9 +++------ tests/test_logic/test_plugin.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 tests/test_logic/test_plugin.py diff --git a/flakehell/_logic/_plugin.py b/flakehell/_logic/_plugin.py index 650f526..4bdc684 100644 --- a/flakehell/_logic/_plugin.py +++ b/flakehell/_logic/_plugin.py @@ -104,17 +104,14 @@ def get_exceptions( if isinstance(path, str): path = Path(path) if root is None: - root = Path() - path.resolve() - path = path.relative_to(root) - path = path.as_posix() - + root = Path().resolve() + path = path.resolve().relative_to(root).as_posix() exceptions = sorted( exceptions.items(), key=lambda item: len(item[0]), reverse=True, ) - for path_rule, rules in exceptions.items(): + for path_rule, rules in exceptions: if path.startswith(path_rule): return rules diff --git a/tests/test_logic/test_plugin.py b/tests/test_logic/test_plugin.py new file mode 100644 index 0000000..f362316 --- /dev/null +++ b/tests/test_logic/test_plugin.py @@ -0,0 +1,19 @@ +from pathlib import Path +from flakehell._logic import get_exceptions + + +def test_get_exceptions(tmp_path: Path): + tests_path = tmp_path / 'tests' + tests_path.mkdir() + test_path = tests_path / 'test_example.py' + test_path.touch() + source_path = tmp_path / 'example.py' + source_path.touch() + + exceptions = { + 'tests/': {'pyflakes': ['+*']}, + } + result = get_exceptions(path=test_path, exceptions=exceptions, root=tmp_path) + assert result == {'pyflakes': ['+*']} + result = get_exceptions(path=source_path, exceptions=exceptions, root=tmp_path) + assert result == {} From 3dcc97a641036e1e88081ae8f85721eb03db814f Mon Sep 17 00:00:00 2001 From: Gram Date: Tue, 19 May 2020 17:37:24 +0200 Subject: [PATCH 5/5] test exclude --- docs/config.md | 26 +++++++++++++++++++++++ flakehell/_patched/_app.py | 4 +++- flakehell/commands/_lint.py | 2 +- tests/test_cli.py | 41 +++++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/docs/config.md b/docs/config.md index b14979d..b0e410a 100644 --- a/docs/config.md +++ b/docs/config.md @@ -16,6 +16,23 @@ Key can be exact plugin name or wildcard template. For example `"flake8-commas"` Value is a list of templates for error codes for this plugin. First symbol in every template must be `+` (include) or `-` (exclude). The latest matched pattern wins. For example, `["+*", "-F*", "-E30?", "-E401"]` means "Include everything except all checks that starts with `F`, check from `E301` to `E310`, and `E401`". +## Exceptions + +Use `exceptions` section to specify special rules for particular paths: + +```toml +[tool.flakehell.plugins] +pycodestyle = ["+*"] +pyflakes = ["+*"] + +[tool.flakehell.exceptions."tests/"] +pycodestyle = ["-F401"] # disable a check +pyflakes = ["-*"] # disable a plugin + +[tool.flakehell.exceptions."tests/test_example.py"] +pyflakes = ["+*"] # enable a plugin +``` + ## Base Option `base` allows to specify base config from which you want to inherit this one. It can be path to local config or remote URL. You can specify one path or list of paths as well. For example: @@ -63,6 +80,15 @@ flake8-bandit = ["-*", "+S1??"] "flake8-*" = ["+*"] # explicitly disable plugin flake8-docstrings = ["-*"] + +# disable some checks for tests +[tool.flakehell.exceptions."tests/"] +pycodestyle = ["-F401"] # disable a check +pyflakes = ["-*"] # disable a plugin + +# do not disable `pyflakes` for one file in tests +[tool.flakehell.exceptions."tests/test_example.py"] +pyflakes = ["+*"] # enable a plugin ``` See [Flake8 documentation](http://flake8.pycqa.org/en/latest/user/configuration.html) to read more about Flake8-specific configuration. diff --git a/flakehell/_patched/_app.py b/flakehell/_patched/_app.py index 02d58ce..164197a 100644 --- a/flakehell/_patched/_app.py +++ b/flakehell/_patched/_app.py @@ -1,5 +1,6 @@ import sys from argparse import ArgumentParser +from itertools import chain from pathlib import Path from typing import Dict, Any, List, Optional, Tuple @@ -27,7 +28,8 @@ def get_toml_config(self, path: Path = None) -> Dict[str, Any]: if path is not None: return read_config(path) # lookup for config from current dir up to root - for dir_path in Path('lol').parents: + root = Path().resolve() + for dir_path in chain([root], root.parents): path = dir_path / 'pyproject.toml' if path.exists(): return read_config(path) diff --git a/flakehell/commands/_lint.py b/flakehell/commands/_lint.py index 4148072..0de928e 100644 --- a/flakehell/commands/_lint.py +++ b/flakehell/commands/_lint.py @@ -11,4 +11,4 @@ def lint_command(argv) -> CommandResult: app.run(argv) app.exit() except SystemExit as exc: - return exc.code, '' + return int(exc.code), '' diff --git a/tests/test_cli.py b/tests/test_cli.py index 5550c74..ea6c5f2 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,10 +1,26 @@ import subprocess +import os import sys +from contextlib import contextmanager +from pathlib import Path +from textwrap import dedent import pytest from flakehell._cli import main +@contextmanager +def chdir(path): + """Context manager for changing dir and restoring previous workdir after exit. + """ + curdir = os.getcwd() + os.chdir(str(path)) + try: + yield + finally: + os.chdir(curdir) + + def test_flake8helled_file(): """Baseline behavior, when an actual filename is passed.""" cmd = [ @@ -57,3 +73,28 @@ def test_lint_help(capsys): assert '-h, --help' in captured.out assert '--builtins' in captured.out assert '--isort-show-traceback' in captured.out + + +def test_exclude(capsys, tmp_path: Path): + text = """ + [tool.flakehell.plugins] + pyflakes = ["+*"] + + [tool.flakehell.exceptions."tests/"] + pyflakes = ["-F401"] + """ + (tmp_path / 'pyproject.toml').write_text(dedent(text)) + (tmp_path / 'example.py').write_text('import sys\na') + (tmp_path / 'tests').mkdir() + (tmp_path / 'tests' / 'test_example.py').write_text('import sys\na') + with chdir(tmp_path): + result = main(['lint', '--format', 'default']) + assert result == (1, '') + captured = capsys.readouterr() + assert captured.err == '' + exp = """ + ./example.py:1:1: F401 'sys' imported but unused + ./example.py:2:1: F821 undefined name 'a' + ./tests/test_example.py:2:1: F821 undefined name 'a' + """ + assert captured.out.strip() == dedent(exp).strip()