Skip to content

Commit

Permalink
Add support for multiline linter results
Browse files Browse the repository at this point in the history
- Fix: multiline results in pylint were not being captured, e.g.
bad-whitespace and bad-continuation
  • Loading branch information
cemsbr committed Apr 26, 2020
1 parent 04ff703 commit 2946d1f
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 30 deletions.
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
Expand All @@ -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/*
Expand Down
34 changes: 18 additions & 16 deletions tests/acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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')

Expand Down
16 changes: 12 additions & 4 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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/
Expand Down
7 changes: 6 additions & 1 deletion yala/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 5 additions & 4 deletions yala/linters.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,11 @@ class Pylint(Linter):

def parse(self, lines):
"""Get :class:`base.Result` parameters using regex."""
pattern = re.compile(r"""^(?P<path>.+?)
:(?P<msg>.+)
:(?P<line_nr>\d+?)
:(?P<col>\d+?)$""", re.VERBOSE)
pattern = re.compile(r'''
.*?^(?P<path>[^\n]+?)
:(?P<msg>.+)
:(?P<line_nr>\d+?)
:(?P<col>\d+?)$''', re.X | re.M | re.S)
return self._parse_by_pattern(lines, pattern)


Expand Down

0 comments on commit 2946d1f

Please sign in to comment.