Skip to content

Commit

Permalink
Implement MockChecker
Browse files Browse the repository at this point in the history
Note on message ids:

When using an already existing message-id, pylint will raise an error.
The message-ids of custom checkers appear to be starting at 9999
counting backwards, so I just took the next available id.

For reference: I looked at
`scripts/dev/pylint_checkers/qute_pylint/config.py::ConfigChecker`,
whose id is 9998.
  • Loading branch information
pylbrecht committed Jun 17, 2022
1 parent 3b81525 commit fa250f9
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 0 deletions.
81 changes: 81 additions & 0 deletions scripts/dev/pylint_checkers/qute_pylint/mock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:

# Copyright 2014-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.

"""Custom astroid checker for mock calls."""

from qutebrowser.utils import utils

import astroid
from pylint import interfaces, checkers


class MockChecker(checkers.BaseChecker):

"""Custom astroid checker for mock calls."""

__implements__ = interfaces.IAstroidChecker
name = 'mock'
msgs = {
'E9997': ('Mock/MagicMock requires the spec argument', # flake8: disable=S001
'mock-requires-spec',
None),
}
priority = -1
printed_warning = False

def _is_mock_call(self, node):
"""Check if node is a mock call.
Args:
node: the currently visited node
Returns:
True if `node` is a mock call
"""
if isinstance(node.func, astroid.nodes.node_classes.Attribute):
# namespaced imports, e.g.
# >>> import unittest.mock
# >>> unittest.mock.Mock()
return node.func.attrname in ('Mock', 'MagicMock')
elif isinstance(node.func, astroid.nodes.node_classes.Name):
# imports without namespace, e.g.
# >>> from unittest.mock import Mock
# >>> Mock()
return node.func.name in ('Mock', 'MagicMock')

raise utils.Unreachable()

def visit_call(self, node: astroid.nodes.Call):
"""Emit a message for mock calls without a spec argument.
Args:
node: the currently visited node
"""
if not self._is_mock_call(node):
return

if any(kw.arg == 'spec' for kw in node.keywords):
return

self.add_message('mock-requires-spec', node=node)


def register(linter):
"""Register this checker."""
linter.register_checker(MockChecker(linter))
80 changes: 80 additions & 0 deletions tests/unit/scripts/test_pylint_checkers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:

# Copyright 2015-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.

"""Tests for custom pylint checkers."""


import astroid
import pylint.testutils
import pytest

from scripts.dev.pylint_checkers.qute_pylint.mock import MockChecker


class TestMockChecker(pylint.testutils.CheckerTestCase):
CHECKER_CLASS = MockChecker

@pytest.mark.parametrize(
'call',
[
'Mock()',
'MagicMock()',
]
)
@pytest.mark.parametrize(
'module_path',
[
'',
'mock.'
'unittest.mock.'
]
)
def test_find_mocks_with_missing_spec(self, module_path, call):
call_node = astroid.extract_node(f'{module_path}{call}')

with self.assertAddsMessages(
pylint_testutils.MessageTest(
msg_id='mock-requires-spec',
node=call_node,
),
):
self.checker.visit_call(call_node)

@pytest.mark.parametrize(
'class_',
[
'Mock(spec=int)',
'MagicMock(spec=int)',
'mock.Mock(spec=int)',
'mock.MagicMock(spec=int)',
'unittest.mock.Mock(spec=int)',
'unittest.mock.MagicMock(spec=int)',
]
)
def test_ignores_mocks_with_spec(self, class_):
call_node = astroid.extract_node(class_)

with self.assertNoMessages():
self.checker.visit_call(call_node)

def test_ignores_non_mocks(self):
call_node = astroid.extract_node('open()')

with self.assertNoMessages():
self.checker.visit_call(call_node)

0 comments on commit fa250f9

Please sign in to comment.