Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds mypy linter #150

Merged
merged 3 commits into from Apr 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -12,6 +12,7 @@
.tox
.vimrc
/.cache
/.mypy_cache
/.ropeproject
/_
/libs
Expand Down
2 changes: 2 additions & 0 deletions README.rst
Expand Up @@ -13,6 +13,7 @@ Code audit tool for Python and JavaScript. Pylama wraps these tools:
* Radon_ © Michele Lacchia
* gjslint_ © The Closure Linter Authors (should be installed 'pylama_gjslint' module);
* eradicate_ © Steven Myint;
* Mypy_ © Jukka Lehtosalo and contributors;

.. _badges:

Expand Down Expand Up @@ -402,6 +403,7 @@ Licensed under a `BSD license`_.
.. _gjslint: https://developers.google.com/closure/utilities
.. _klen: http://klen.github.io/
.. _eradicate: https://github.com/myint/eradicate
.. _Mypy: https://github.com/python/mypy

.. |logo| image:: https://raw.github.com/klen/pylama/develop/docs/_static/logo.png
:width: 100
Expand Down
10 changes: 10 additions & 0 deletions dummy.py
Expand Up @@ -125,3 +125,13 @@ def __init__(self, filename, lineno, names):
Message.__init__(self, filename, lineno)
self.message_args = (names,)
error = 1 # noQa and some comments


class BadTyping(Message):
"""Test the MyPy linting."""

message = 'error: No return value expected'

def bad_method(self): # type: () -> None
"""Return type mismatch."""
return 1
6 changes: 6 additions & 0 deletions pylama/lint/extensions.py
Expand Up @@ -46,6 +46,12 @@
except ImportError:
pass

try:
from pylama.lint.pylama_mypy import Linter
LINTERS['mypy'] = Linter()
except ImportError:
pass


from pkg_resources import iter_entry_points

Expand Down
88 changes: 88 additions & 0 deletions pylama/lint/pylama_mypy.py
@@ -0,0 +1,88 @@
"""MyPy support."""

from mypy import api

from pylama.lint import Linter as Abstract


class _MyPyMessage(object):
"""Parser for a single MyPy output line."""
types = {
'error': 'E',
'warning': 'W',
'note': 'N'
}

def __init__(self, line):
self.filename = None
self.line_num = None
self.column = None
self.text = None
self.note = None
self.message_type = None
self.valid = False

self._parse(line)

def _parse(self, line):
"""Parse the output line"""
try:
result = line.split(':', maxsplit=4)
filename, line_num_txt, column_txt, message_type, text = result
except ValueError:
return

try:
self.line_num = int(line_num_txt.strip())
self.column = int(column_txt.strip())
except ValueError:
return

self.filename = filename
self.message_type = message_type.strip()
self.text = text.strip()
self.valid = True

def add_note(self, note):
"""Add in additional information about this message"""
self.note = note

def to_result(self):
"""Convert to the Linter.run return value"""
text = [self.text]
if self.note:
text.append(self.note)

return {
'lnum': self.line_num,
'col': self.column,
'text': ' - '.join(text),
'type': self.types.get(self.message_type, '')
}


class Linter(Abstract):
"""MyPy runner."""

@staticmethod
def run(path, code=None, params=None, **meta):
"""Check code with mypy.

:return list: List of errors.
"""
args = [path, '--follow-imports=skip', '--show-column-numbers']
stdout, stderr, status = api.run(args)
messages = []
for line in stdout.split('\n'):
line.strip()
if not line:
continue
message = _MyPyMessage(line)
if message.valid:
if message.message_type == 'note':
if messages[-1].line_num == message.line_num:
messages[-1].add_note(message.text)
else:
messages.append(message)

return [m.to_result() for m in messages]
1 change: 1 addition & 0 deletions requirements-test.txt
Expand Up @@ -4,3 +4,4 @@ ipdb
pytest
eradicate >= 0.2
radon >= 1.4.2
mypy ; python_version >= '3.5'
2 changes: 1 addition & 1 deletion tests/test_config.py
Expand Up @@ -28,7 +28,7 @@ def test_ignore_select():
options.ignore = ['E301', 'D102']
options.linters = ['pycodestyle', 'pydocstyle', 'pyflakes', 'mccabe']
errors = run('dummy.py', options=options)
assert len(errors) == 31
assert len(errors) == 32

numbers = [error.number for error in errors]
assert 'D100' in numbers
Expand Down
10 changes: 10 additions & 0 deletions tests/test_linters.py
@@ -1,3 +1,5 @@
import sys

from pylama.config import parse_options
from pylama.core import run
from pylama.lint.extensions import LINTERS
Expand Down Expand Up @@ -52,3 +54,11 @@ def test_pydocstyle():
assert len(options.linters) == 1
errors = run('dummy.py', options=options)
assert errors


def test_mypy():
if sys.version_info.major >= 3 and sys.version_info.minor >= 5:
options = parse_options(linters=['mypy'])
assert len(options.linters) == 1
errors = run('dummy.py', options=options)
assert len(errors) == 1