From 2946d1f2e529d98b8c7578e4cc2a11b4f16dadc1 Mon Sep 17 00:00:00 2001 From: "Carlos Eduardo M. Santos" Date: Sun, 26 Apr 2020 17:26:31 -0300 Subject: [PATCH] Add support for multiline linter results - Fix: multiline results in pylint were not being captured, e.g. bad-whitespace and bad-continuation --- CHANGELOG.md | 12 +++++++++++- Makefile | 4 ---- tests/acceptance.py | 34 ++++++++++++++++++---------------- tox.ini | 16 ++++++++++++---- yala/base.py | 7 ++++++- yala/linters.py | 9 +++++---- 6 files changed, 52 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6789db..a7363de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +## [2.2.1] - 2020-04-26 +### Fixed +- Multiline results in pylint were not being captured, e.g. bad-whitespace and + bad-continuation. + +### Changed +- Pipfile has Python 3.8 now that it is available in Ubuntu LTS 20.04. However, + CI still tests under 3.5, 3.6, and 3.7, too. + ## [2.2.0] - 2019-11-28 ### Added - Pylint as default (required) linter (as in v1) @@ -37,7 +46,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Unused code to parse pyflakes and radon - dev: dependency management via requirements file (use pipenv) -[Unreleased]: https://github.com/cemsbr/yala/compare/v2.2.0...HEAD +[Unreleased]: https://github.com/cemsbr/yala/compare/v2.2.1...HEAD +[2.3.0]: https://github.com/cemsbr/yala/compare/v2.2.0...v2.2.1 [2.2.0]: https://github.com/cemsbr/yala/compare/v2.1.0...v2.2.0 [2.1.0]: https://github.com/cemsbr/yala/compare/v2.0.0...v2.1.0 [2.0.0]: https://github.com/cemsbr/yala/compare/v1.8.0...v2.0.0 diff --git a/Makefile b/Makefile index d41f07a..0cf7550 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,6 @@ help: @echo '=======' @echo ' clean: Remove generated files.' @echo ' help: This message.' - @echo "update-deps: Update dev packages' pinned versions." @echo ' upload: Upload signed Python package to PyPI.' @echo -n ' watch: When code changes, run tests.' @echo -n ' sonarqube: Update SonarQube code metrics.' @@ -12,9 +11,6 @@ help: clean: rm -rf .eggs/ .tox/ build/ dist/ yala.egg-info/ -update-deps: - pipenv update - upload: clean python setup.py clean sdist bdist_wheel twine upload dist/* diff --git a/tests/acceptance.py b/tests/acceptance.py index 7636185..c5d9591 100644 --- a/tests/acceptance.py +++ b/tests/acceptance.py @@ -28,33 +28,32 @@ def setUpClass(cls, pool_mock, stdout_mock, exit_mock): pool_mock.return_value = ThreadPoolExecutor() with patch('yala.main.sys.argv', ['yala', 'tests_data/fake_code.py']): main() - output = stdout_mock.getvalue() - # Remove empty last line due to trailing '\n' - cls._output = output.split('\n')[:-1] + cls._output = stdout_mock.getvalue() def _assert_results(self, lines, linter_name): """Assert all lines are in the output.""" for line in lines: self._assert_result(line, linter_name) - def _assert_result(self, line, linter_name): - self.assertTrue(self._output_has_line(line, linter_name), - 'Couldn\'t match:\n {}\nOutput:\n {}'.format( - line, '\n '.join(self._output))) + def _assert_result(self, result, linter_name): + self.assertTrue(self._output_has_result(result, linter_name), + 'Couldn\'t match:\n{}\nOutput:\n{}'.format( + result, self._output)) - def _assert_any_result(self, lines, linter_name): - any_line = any(self._output_has_line(l, linter_name) for l in lines) - self.assertTrue(any_line, 'None found:\n {}\nOutput:\n {}'.format( - '\n '.join(lines), '\n '.join(self._output))) + def _assert_any_result(self, results, linter_name): + first_result = any(self._output_has_result(r, linter_name) + for r in results) + self.assertTrue(first_result, 'None matched:\n{}\nOutput:\n{}' + .format('\n'.join(results), self._output)) - def _output_has_line(self, line, linter_name): - expected_regex = self._get_expected_regex(line, linter_name) - return any(re.match(expected_regex, out) for out in self._output) + def _output_has_result(self, result, linter_name): + expected_regex = self._get_expected_regex(result, linter_name) + return re.match(expected_regex, self._output, re.M | re.S) is not None @staticmethod def _get_expected_regex(line, linter_name): """Return a regex to match both Linux and Windows paths.""" - regex = r'tests_data[/\\]fake_code.py\|{} \[{}\]' + regex = r'.*?^tests_data[/\\]fake_code.py\|{} \[{}\]$' escaped_output = re.escape(line) return regex.format(escaped_output, linter_name) @@ -110,7 +109,10 @@ def test_pylint(self): '1:0|Missing module docstring (C0114, missing-module-docstring)', '1:0|Unused import os (W0611, unused-import)', '2:0|Unused import abc (W0611, unused-import)', - '7:0|Too many branches (20/12) (R0912, too-many-branches)' + '7:0|Too many branches (20/12) (R0912, too-many-branches)', + '7:20|No space allowed before bracket\n' + 'def high_complexity (arg):\n' + ' ^ (C0326, bad-whitespace)' ) self._assert_results(expected, 'pylint') diff --git a/tox.ini b/tox.ini index 71aac7c..a5d98d0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,12 @@ [tox] -envlist = py35,py36,py37 +envlist = py35,py36,py37,py38 skip_missing_interpreters = true [testenv] whitelist_externals = rm -[testenv:py37] +[testenv:py38] +; Code checks and linters are run in the latest Python version only. parallel_show_output = true commands= ; Force packaging even if setup.{py,cfg} haven't changed @@ -24,8 +25,8 @@ commands= coverage report [testenv:up_analysis] -; py37 runs coverage -depends = py37 +; Upload coverage data +depends = py38 parallel_show_output = true passenv= CODECOV_TOKEN @@ -42,6 +43,13 @@ commands= codecov python-codacy-coverage -r coverage.xml +[testenv:py37] +commands= + rm -rf ./yala.egg-info/ + pip install -U pip + pip install -U .[all,dev] + python setup.py test + [testenv:py36] commands= rm -rf ./yala.egg-info/ diff --git a/yala/base.py b/yala/base.py index 1621f54..8d9fbd6 100644 --- a/yala/base.py +++ b/yala/base.py @@ -126,13 +126,18 @@ def _parse_by_pattern(self, lines, pattern): generator: Result instances. """ + buffer = '' # lines is an iterable, but there may be multiline matches for line in lines: - match = pattern.match(line) + buffer += line + match = pattern.match(buffer) if match: + buffer = '' # clear buffer after a match params = match.groupdict() if not params: params = match.groups() yield self._create_output_from_match(params) + else: + buffer += '\n' # keep lines separated by a newline def _create_output_from_match(self, match_result): """Create Result instance from pattern match results. diff --git a/yala/linters.py b/yala/linters.py index 2655121..3fc2acd 100644 --- a/yala/linters.py +++ b/yala/linters.py @@ -114,10 +114,11 @@ class Pylint(Linter): def parse(self, lines): """Get :class:`base.Result` parameters using regex.""" - pattern = re.compile(r"""^(?P.+?) - :(?P.+) - :(?P\d+?) - :(?P\d+?)$""", re.VERBOSE) + pattern = re.compile(r''' + .*?^(?P[^\n]+?) + :(?P.+) + :(?P\d+?) + :(?P\d+?)$''', re.X | re.M | re.S) return self._parse_by_pattern(lines, pattern)