Skip to content

Commit f85eeba

Browse files
committed
Adding base pylint rcfile and plugin for unittests.
1 parent 6dd5f2b commit f85eeba

File tree

4 files changed

+212
-23
lines changed

4 files changed

+212
-23
lines changed

gcloud/test_connection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
class TestConnection(unittest2.TestCase):
55

6-
def _getTargetClass(self):
6+
@staticmethod
7+
def _getTargetClass():
78
from gcloud.connection import Connection
89
return Connection
910

@@ -35,7 +36,6 @@ def test_http_w_creds(self):
3536
authorized = object()
3637

3738
class Creds(object):
38-
3939
def authorize(self, http):
4040
self._called_with = http
4141
return authorized

gcloud/test_credentials.py

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
class TestCredentials(unittest2.TestCase):
55

6-
def _getTargetClass(self):
6+
@staticmethod
7+
def _getTargetClass():
78
from gcloud.credentials import Credentials
89
return Credentials
910

@@ -15,16 +16,16 @@ def test_get_for_service_account_wo_scope(self):
1516
cls = self._getTargetClass()
1617
client = _Client()
1718
with _Monkey(credentials, client=client):
18-
with NamedTemporaryFile() as f:
19-
f.write(PRIVATE_KEY)
20-
f.flush()
21-
found = cls.get_for_service_account(CLIENT_EMAIL, f.name)
19+
with NamedTemporaryFile() as file_obj:
20+
file_obj.write(PRIVATE_KEY)
21+
file_obj.flush()
22+
found = cls.get_for_service_account(CLIENT_EMAIL,
23+
file_obj.name)
2224
self.assertTrue(found is client._signed)
23-
self.assertEqual(client._called_with,
24-
{'service_account_name': CLIENT_EMAIL,
25-
'private_key': PRIVATE_KEY,
26-
'scope': None,
27-
})
25+
expected_called_with = {'service_account_name': CLIENT_EMAIL,
26+
'private_key': PRIVATE_KEY,
27+
'scope': None}
28+
self.assertEqual(client._called_with, expected_called_with)
2829

2930
def test_get_for_service_account_w_scope(self):
3031
from tempfile import NamedTemporaryFile
@@ -35,21 +36,19 @@ def test_get_for_service_account_w_scope(self):
3536
cls = self._getTargetClass()
3637
client = _Client()
3738
with _Monkey(credentials, client=client):
38-
with NamedTemporaryFile() as f:
39-
f.write(PRIVATE_KEY)
40-
f.flush()
41-
found = cls.get_for_service_account(CLIENT_EMAIL, f.name,
42-
SCOPE)
39+
with NamedTemporaryFile() as file_obj:
40+
file_obj.write(PRIVATE_KEY)
41+
file_obj.flush()
42+
found = cls.get_for_service_account(CLIENT_EMAIL,
43+
file_obj.name, SCOPE)
4344
self.assertTrue(found is client._signed)
44-
self.assertEqual(client._called_with,
45-
{'service_account_name': CLIENT_EMAIL,
46-
'private_key': PRIVATE_KEY,
47-
'scope': SCOPE,
48-
})
45+
expected_called_with = {'service_account_name': CLIENT_EMAIL,
46+
'private_key': PRIVATE_KEY,
47+
'scope': SCOPE}
48+
self.assertEqual(client._called_with, expected_called_with)
4949

5050

5151
class _Client(object):
52-
5352
def __init__(self):
5453
self._signed = object()
5554

@@ -59,6 +58,7 @@ def SignedJwtAssertionCredentials(self, **kw):
5958

6059

6160
class _Monkey(object):
61+
6262
# context-manager for replacing module names in the scope of a test.
6363

6464
def __init__(self, module, **kw):

pylint_unittest_checker.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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))

pylintrc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[FORMAT]
2+
no-space-check=
3+
[MASTER]
4+
load-plugins=pylint_unittest_checker
5+
[MESSAGES CONTROL]
6+
disable=I,protected-access
7+
[REPORTS]
8+
reports=no
9+
[VARIABLES]
10+
dummy-variables-rgx=_$|dummy|^unused_

0 commit comments

Comments
 (0)