1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import ast
import re
from hacking import core
@core.off_by_default
@core.flake8ext
class MockAutospecCheck(object):
"""Check for 'autospec' in mock.patch/mock.patch.object calls
Okay: mock.patch('target_module_1', autospec=True)
Okay: mock.patch('target_module_1', autospec=False)
Okay: mock.patch('target_module_1', autospec=None)
Okay: mock.patch('target_module_1', defined_mock)
Okay: mock.patch('target_module_1', new=defined_mock)
Okay: mock.patch('target_module_1', new_callable=SomeFunc)
Okay: mock.patch('target_module_1', defined_mock)
Okay: mock.patch('target_module_1', spec=1000)
Okay: mock.patch('target_module_1', spec_set=['data'])
Okay: mock.patch('target_module_1', wraps=some_obj)
H210: mock.patch('target_module_1')
Okay: mock.patch('target_module_1') # noqa
H210: mock.patch('target_module_1', somearg=23)
Okay: mock.patch('target_module_1', somearg=23) # noqa
Okay: mock.patch.object('target_module_2', 'attribute', autospec=True)
Okay: mock.patch.object('target_module_2', 'attribute', autospec=False)
Okay: mock.patch.object('target_module_2', 'attribute', autospec=None)
Okay: mock.patch.object('target_module_2', 'attribute', new=defined_mock)
Okay: mock.patch.object('target_module_2', 'attribute', defined_mock)
Okay: mock.patch.object('target_module_2', 'attribute', new_callable=AFunc)
Okay: mock.patch.object('target_module_2', 'attribute', spec=3)
Okay: mock.patch.object('target_module_2', 'attribute', spec_set=[3])
Okay: mock.patch.object('target_module_2', 'attribute', wraps=some_obj)
H210: mock.patch.object('target_module_2', 'attribute', somearg=2)
H210: mock.patch.object('target_module_2', 'attribute')
"""
name = "mock_check"
version = "1.00"
def __init__(self, tree, filename):
self.filename = filename
self.tree = tree
def run(self):
mcv = MockCheckVisitor(self.filename)
mcv.visit(self.tree)
for message in mcv.messages:
yield message
class MockCheckVisitor(ast.NodeVisitor):
# Patchers we are looking for and minimum number of 'args' without
# 'autospec' to not be flagged
patchers = {'mock.patch': 2, 'mock.patch.object': 3}
spec_keywords = {"autospec", "new", "new_callable", "spec", "spec_set",
"wraps"}
def __init__(self, filename):
super(MockCheckVisitor, self).__init__()
self.messages = []
self.filename = filename
def check_missing_autospec(self, call_node):
def find_autospec_keyword(keyword_node):
for keyword_obj in keyword_node:
keyword = keyword_obj.arg
# If they have defined autospec or new then it is okay
if keyword in self.spec_keywords:
return True
return False
if isinstance(call_node, ast.Call):
func_info = FunctionNameFinder(self.filename)
func_info.visit(call_node)
# We are only looking at our patchers
if func_info.function_name not in self.patchers:
return
min_args = self.patchers[func_info.function_name]
if not find_autospec_keyword(call_node.keywords):
if len(call_node.args) < min_args:
self.messages.append(
(call_node.lineno, call_node.col_offset,
"H210 Missing 'autospec' or 'spec_set' keyword in "
"mock.patch/mock.patch.object", MockCheckVisitor)
)
def visit_Call(self, node):
self.check_missing_autospec(node)
self.generic_visit(node)
class FunctionNameFinder(ast.NodeVisitor):
"""Finds the name of the function"""
def __init__(self, filename):
super(FunctionNameFinder, self).__init__()
self._func_name = []
self.filename = filename
@property
def function_name(self):
return '.'.join(reversed(self._func_name))
def visit_Name(self, node):
self._func_name.append(node.id)
self.generic_visit(node)
def visit_Attribute(self, node):
try:
self._func_name.append(node.attr)
self._func_name.append(node.value.id)
except AttributeError:
self.generic_visit(node)
def visit(self, node):
# If we get called with an ast.Call node, then work on the 'node.func',
# as we want the function name.
if isinstance(node, ast.Call):
return super(FunctionNameFinder, self).visit(node.func)
return super(FunctionNameFinder, self).visit(node)
third_party_mock = re.compile('^import.mock')
from_third_party_mock = re.compile('^from.mock.import')
@core.flake8ext
def hacking_no_third_party_mock(logical_line, noqa):
"""Check for use of mock instead of unittest.mock.
Projects have had issues with using mock without including it in their
requirements, thinking it is the standard library version. This makes it so
these projects need to explicitly turn off this check to make it clear they
intended to use it.
Okay: from unittest import mock
Okay: from unittest.mock import patch
Okay: import unittest.mock
H216: import mock
H216: from mock import patch
Okay: try: import mock
Okay: import mock # noqa
"""
msg = ('H216: The unittest.mock module should be used rather than the '
'third party mock package unless actually needed. If so, disable '
'the H216 check in hacking config and ensure mock is declared in '
"the project's requirements.")
if noqa:
return
if (re.match(third_party_mock, logical_line) or
re.match(from_third_party_mock, logical_line)):
yield (0, msg)
|