|
| 1 | +"""Plugin for pylint to suppress warnings on tests. |
| 2 | +
|
| 3 | +Turns off the following pylint errors/warnings: |
| 4 | +- Docstring checks in test modules. |
| 5 | +- Too few public methods on test classes. |
| 6 | +- Too many public methods on subclasses of unittest.TestCase. |
| 7 | +- Invalid names on all functions/methods. |
| 8 | +- Private attribute mangling outside __init__ method. |
| 9 | +- Invalid variable name assignment. |
| 10 | +""" |
| 11 | + |
| 12 | +import importlib |
| 13 | +import unittest |
| 14 | +import unittest2 |
| 15 | + |
| 16 | +import astroid |
| 17 | +import pylint.checkers |
| 18 | +import pylint.interfaces |
| 19 | + |
| 20 | + |
| 21 | +MAX_PUBLIC_METHODS = 20 |
| 22 | + |
| 23 | + |
| 24 | +def is_test_module(checker, module_node): |
| 25 | + """Boolean to determine if a module is a test module.""" |
| 26 | + return checker.get_test_module(module_node) is not None |
| 27 | + |
| 28 | + |
| 29 | +def get_unittest_class(checker, class_node): |
| 30 | + """Get a corresponding Python class object for a TestCase subclass.""" |
| 31 | + module_obj = checker.get_test_module(class_node.root()) |
| 32 | + if module_obj is None: |
| 33 | + return |
| 34 | + |
| 35 | + class_obj = getattr(module_obj, class_node.name, None) |
| 36 | + try: |
| 37 | + if issubclass(class_obj, |
| 38 | + (unittest.TestCase, unittest2.TestCase)): |
| 39 | + return class_obj |
| 40 | + except TypeError: |
| 41 | + pass |
| 42 | + |
| 43 | + |
| 44 | +def suppress_too_many_methods(checker, class_node): |
| 45 | + """Suppress too-many-public-methods warnings on a TestCase. |
| 46 | +
|
| 47 | + Checks that the current class (`class_node`) is a subclass |
| 48 | + of unittest.TestCase or unittest2.TestCase before suppressing. |
| 49 | +
|
| 50 | + To make reasonable, still checks the number of public methods defined |
| 51 | + explicitly on the subclass. |
| 52 | + """ |
| 53 | + class_obj = get_unittest_class(checker, class_node) |
| 54 | + if class_obj is None: |
| 55 | + return |
| 56 | + |
| 57 | + checker.linter.disable('too-many-public-methods', |
| 58 | + scope='module', line=class_node.fromlineno) |
| 59 | + |
| 60 | + # Count the number of public methods defined locally. |
| 61 | + nb_public_methods = 0 |
| 62 | + for method in class_node.methods(): |
| 63 | + if (method.name in class_obj.__dict__ and |
| 64 | + not method.name.startswith('_')): |
| 65 | + nb_public_methods += 1 |
| 66 | + |
| 67 | + # Add a message if we exceed MAX_PUBLIC_METHODS. |
| 68 | + if nb_public_methods > MAX_PUBLIC_METHODS: |
| 69 | + checker.add_message('too-many-public-methods', node=class_node, |
| 70 | + args=(nb_public_methods, |
| 71 | + MAX_PUBLIC_METHODS)) |
| 72 | + |
| 73 | + |
| 74 | +def suppress_too_few_methods(checker, class_node): |
| 75 | + """Suppress too-few-public-methods warnings on test classes.""" |
| 76 | + if not is_test_module(checker, class_node.root()): |
| 77 | + return |
| 78 | + |
| 79 | + checker.linter.disable('too-few-public-methods', |
| 80 | + scope='module', line=class_node.fromlineno) |
| 81 | + |
| 82 | + |
| 83 | +def suppress_invalid_fn_name(checker, function_node): |
| 84 | + """Suppress invalid-name warnings on method names in a test.""" |
| 85 | + if not is_test_module(checker, function_node.root()): |
| 86 | + return |
| 87 | + |
| 88 | + checker.linter.disable('invalid-name', scope='module', |
| 89 | + line=function_node.fromlineno) |
| 90 | + |
| 91 | + |
| 92 | +def transform_ignored_docstrings(checker, astroid_obj): |
| 93 | + """Module/Class/Function transformer to ignore docstrings. |
| 94 | +
|
| 95 | + The astroid object is edited so as to appear that a docstring |
| 96 | + is set. |
| 97 | + """ |
| 98 | + if isinstance(astroid_obj, astroid.scoped_nodes.Module): |
| 99 | + module = astroid_obj |
| 100 | + else: |
| 101 | + module = astroid_obj.root() |
| 102 | + |
| 103 | + if not is_test_module(checker, module): |
| 104 | + return |
| 105 | + |
| 106 | + # Fool `pylint` by setting a dummy docstring. |
| 107 | + if astroid_obj.doc in ('', None): |
| 108 | + astroid_obj.doc = 'NOT EMPTY STRING.' |
| 109 | + |
| 110 | + |
| 111 | +class UnittestChecker(pylint.checkers.BaseChecker): |
| 112 | + """Checker for unit test modules.""" |
| 113 | + |
| 114 | + __implements__ = pylint.interfaces.IAstroidChecker |
| 115 | + |
| 116 | + name = 'unittest_checker' |
| 117 | + # `msgs` must be non-empty to register successfully. We spoof an error |
| 118 | + # message string of length 5. |
| 119 | + msgs = {'E_FLS': ('%r', 'UNUSED')} |
| 120 | + |
| 121 | + # So that this checker is executed before others, even the name checker. |
| 122 | + priority = 0 |
| 123 | + |
| 124 | + def __init__(self, linter=None): |
| 125 | + super(UnittestChecker, self).__init__(linter=linter) |
| 126 | + self._checked_modules = {} |
| 127 | + |
| 128 | + def get_test_module(self, module_node): |
| 129 | + """Gets a corresponding Python module object for a test node. |
| 130 | +
|
| 131 | + The `module_node` is an astroid object from the parsed tree. |
| 132 | +
|
| 133 | + Caches results on instance to memoize work. |
| 134 | + """ |
| 135 | + if module_node not in self._checked_modules: |
| 136 | + module_file = module_node.name.rsplit('.', 1)[-1] |
| 137 | + if module_file.startswith('test'): |
| 138 | + module_obj = importlib.import_module(module_node.name) |
| 139 | + self._checked_modules[module_node] = module_obj |
| 140 | + else: |
| 141 | + self._checked_modules[module_node] = None |
| 142 | + return self._checked_modules[module_node] |
| 143 | + |
| 144 | + def visit_module(self, module_node): |
| 145 | + """Checker specific method when module is linted.""" |
| 146 | + transform_ignored_docstrings(self, module_node) |
| 147 | + |
| 148 | + def visit_class(self, class_node): |
| 149 | + """Checker specific method when class is linted.""" |
| 150 | + transform_ignored_docstrings(self, class_node) |
| 151 | + suppress_too_many_methods(self, class_node) |
| 152 | + suppress_too_few_methods(self, class_node) |
| 153 | + |
| 154 | + def visit_function(self, function_node): |
| 155 | + """Checker specific method when function is linted.""" |
| 156 | + suppress_invalid_fn_name(self, function_node) |
| 157 | + transform_ignored_docstrings(self, function_node) |
| 158 | + |
| 159 | + def visit_assattr(self, assign_attr_node): |
| 160 | + """Checker specific method when attribute assignment is linted.""" |
| 161 | + if not is_test_module(self, assign_attr_node.root()): |
| 162 | + return |
| 163 | + |
| 164 | + if assign_attr_node.attrname.startswith('_'): |
| 165 | + self.linter.disable('attribute-defined-outside-init', |
| 166 | + scope='module', line=assign_attr_node.lineno) |
| 167 | + |
| 168 | + def visit_assname(self, assign_name_node): |
| 169 | + """Checker specific method when variable assignment is linted.""" |
| 170 | + if not is_test_module(self, assign_name_node.root()): |
| 171 | + return |
| 172 | + |
| 173 | + self.linter.disable('invalid-name', scope='module', |
| 174 | + line=assign_name_node.lineno) |
| 175 | + |
| 176 | + |
| 177 | +def register(linter): |
| 178 | + """required method to auto register this checker""" |
| 179 | + linter.register_checker(UnittestChecker(linter)) |
0 commit comments