Skip to content

Commit

Permalink
Merge pull request #150 from ianepperson/develop
Browse files Browse the repository at this point in the history
Adds mypy linter
  • Loading branch information
klen committed Apr 10, 2019
2 parents 837ecd3 + e931957 commit fa552de
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 1 deletion.
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

0 comments on commit fa552de

Please sign in to comment.