Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding base pylint rcfile and plugin for unittests. #221

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions gcloud/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

class TestConnection(unittest2.TestCase):

def _getTargetClass(self):
@staticmethod
def _getTargetClass():
from gcloud.connection import Connection
return Connection

Expand Down Expand Up @@ -35,7 +36,6 @@ def test_http_w_creds(self):
authorized = object()

class Creds(object):

def authorize(self, http):
self._called_with = http
return authorized
Expand Down
42 changes: 21 additions & 21 deletions gcloud/test_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

class TestCredentials(unittest2.TestCase):

def _getTargetClass(self):
@staticmethod
def _getTargetClass():
from gcloud.credentials import Credentials
return Credentials

Expand All @@ -15,16 +16,16 @@ def test_get_for_service_account_wo_scope(self):
cls = self._getTargetClass()
client = _Client()
with _Monkey(credentials, client=client):
with NamedTemporaryFile() as f:
f.write(PRIVATE_KEY)
f.flush()
found = cls.get_for_service_account(CLIENT_EMAIL, f.name)
with NamedTemporaryFile() as file_obj:

This comment was marked as spam.

This comment was marked as spam.

file_obj.write(PRIVATE_KEY)
file_obj.flush()
found = cls.get_for_service_account(CLIENT_EMAIL,
file_obj.name)
self.assertTrue(found is client._signed)
self.assertEqual(client._called_with,
{'service_account_name': CLIENT_EMAIL,
'private_key': PRIVATE_KEY,
'scope': None,
})
expected_called_with = {'service_account_name': CLIENT_EMAIL,

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

'private_key': PRIVATE_KEY,
'scope': None}
self.assertEqual(client._called_with, expected_called_with)

def test_get_for_service_account_w_scope(self):
from tempfile import NamedTemporaryFile
Expand All @@ -35,21 +36,19 @@ def test_get_for_service_account_w_scope(self):
cls = self._getTargetClass()
client = _Client()
with _Monkey(credentials, client=client):
with NamedTemporaryFile() as f:
f.write(PRIVATE_KEY)
f.flush()
found = cls.get_for_service_account(CLIENT_EMAIL, f.name,
SCOPE)
with NamedTemporaryFile() as file_obj:
file_obj.write(PRIVATE_KEY)
file_obj.flush()
found = cls.get_for_service_account(CLIENT_EMAIL,
file_obj.name, SCOPE)
self.assertTrue(found is client._signed)
self.assertEqual(client._called_with,
{'service_account_name': CLIENT_EMAIL,
'private_key': PRIVATE_KEY,
'scope': SCOPE,
})
expected_called_with = {'service_account_name': CLIENT_EMAIL,
'private_key': PRIVATE_KEY,
'scope': SCOPE}
self.assertEqual(client._called_with, expected_called_with)


class _Client(object):

def __init__(self):

This comment was marked as spam.

This comment was marked as spam.

self._signed = object()

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


class _Monkey(object):

# context-manager for replacing module names in the scope of a test.

def __init__(self, module, **kw):
Expand Down
179 changes: 179 additions & 0 deletions pylint_unittest_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
"""Plugin for pylint to suppress warnings on tests.

Turns off the following pylint errors/warnings:
- Docstring checks in test modules.
- Too few public methods on test classes.
- Too many public methods on subclasses of unittest.TestCase.
- Invalid names on all functions/methods.
- Private attribute mangling outside __init__ method.
- Invalid variable name assignment.
"""

import importlib
import unittest
import unittest2

import astroid
import pylint.checkers
import pylint.interfaces


MAX_PUBLIC_METHODS = 20


def is_test_module(checker, module_node):
"""Boolean to determine if a module is a test module."""
return checker.get_test_module(module_node) is not None


def get_unittest_class(checker, class_node):
"""Get a corresponding Python class object for a TestCase subclass."""
module_obj = checker.get_test_module(class_node.root())
if module_obj is None:

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

return

class_obj = getattr(module_obj, class_node.name, None)

This comment was marked as spam.

This comment was marked as spam.

try:
if issubclass(class_obj,
(unittest.TestCase, unittest2.TestCase)):
return class_obj
except TypeError:
pass


def suppress_too_many_methods(checker, class_node):
"""Suppress too-many-public-methods warnings on a TestCase.

Checks that the current class (`class_node`) is a subclass
of unittest.TestCase or unittest2.TestCase before suppressing.

To make reasonable, still checks the number of public methods defined
explicitly on the subclass.
"""
class_obj = get_unittest_class(checker, class_node)
if class_obj is None:

This comment was marked as spam.

return

checker.linter.disable('too-many-public-methods',
scope='module', line=class_node.fromlineno)

# Count the number of public methods defined locally.
nb_public_methods = 0
for method in class_node.methods():
if (method.name in class_obj.__dict__ and
not method.name.startswith('_')):
nb_public_methods += 1

# Add a message if we exceed MAX_PUBLIC_METHODS.
if nb_public_methods > MAX_PUBLIC_METHODS:
checker.add_message('too-many-public-methods', node=class_node,
args=(nb_public_methods,
MAX_PUBLIC_METHODS))


def suppress_too_few_methods(checker, class_node):
"""Suppress too-few-public-methods warnings on test classes."""
if not is_test_module(checker, class_node.root()):

This comment was marked as spam.

return

checker.linter.disable('too-few-public-methods',
scope='module', line=class_node.fromlineno)


def suppress_invalid_fn_name(checker, function_node):
"""Suppress invalid-name warnings on method names in a test."""
if not is_test_module(checker, function_node.root()):

This comment was marked as spam.

return

checker.linter.disable('invalid-name', scope='module',
line=function_node.fromlineno)


def transform_ignored_docstrings(checker, astroid_obj):
"""Module/Class/Function transformer to ignore docstrings.

The astroid object is edited so as to appear that a docstring
is set.
"""
if isinstance(astroid_obj, astroid.scoped_nodes.Module):
module = astroid_obj
else:
module = astroid_obj.root()

if not is_test_module(checker, module):

This comment was marked as spam.

This comment was marked as spam.

return

# Fool `pylint` by setting a dummy docstring.
if astroid_obj.doc in ('', None):
astroid_obj.doc = 'NOT EMPTY STRING.'


class UnittestChecker(pylint.checkers.BaseChecker):
"""Checker for unit test modules."""

__implements__ = pylint.interfaces.IAstroidChecker

name = 'unittest_checker'
# `msgs` must be non-empty to register successfully. We spoof an error
# message string of length 5.
msgs = {'E_FLS': ('%r', 'UNUSED')}

# So that this checker is executed before others, even the name checker.
priority = 0

def __init__(self, linter=None):
super(UnittestChecker, self).__init__(linter=linter)
self._checked_modules = {}

def get_test_module(self, module_node):
"""Gets a corresponding Python module object for a test node.

The `module_node` is an astroid object from the parsed tree.

Caches results on instance to memoize work.
"""
if module_node not in self._checked_modules:
module_file = module_node.name.rsplit('.', 1)[-1]
if module_file.startswith('test'):
module_obj = importlib.import_module(module_node.name)
self._checked_modules[module_node] = module_obj
else:
self._checked_modules[module_node] = None
return self._checked_modules[module_node]

def visit_module(self, module_node):
"""Checker specific method when module is linted."""
transform_ignored_docstrings(self, module_node)

def visit_class(self, class_node):
"""Checker specific method when class is linted."""
transform_ignored_docstrings(self, class_node)
suppress_too_many_methods(self, class_node)
suppress_too_few_methods(self, class_node)

def visit_function(self, function_node):
"""Checker specific method when function is linted."""
suppress_invalid_fn_name(self, function_node)
transform_ignored_docstrings(self, function_node)

def visit_assattr(self, assign_attr_node):
"""Checker specific method when attribute assignment is linted."""
if not is_test_module(self, assign_attr_node.root()):

This comment was marked as spam.

return

if assign_attr_node.attrname.startswith('_'):
self.linter.disable('attribute-defined-outside-init',
scope='module', line=assign_attr_node.lineno)

def visit_assname(self, assign_name_node):
"""Checker specific method when variable assignment is linted."""
if not is_test_module(self, assign_name_node.root()):

This comment was marked as spam.

return

self.linter.disable('invalid-name', scope='module',
line=assign_name_node.lineno)


def register(linter):
"""required method to auto register this checker"""
linter.register_checker(UnittestChecker(linter))
10 changes: 10 additions & 0 deletions pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[FORMAT]
no-space-check=
[MASTER]
load-plugins=pylint_unittest_checker
[MESSAGES CONTROL]
disable=I,protected-access
[REPORTS]
reports=no
[VARIABLES]
dummy-variables-rgx=_$|dummy|^unused_