diff options
author | bdanielby <Bruno.Daniel@blue-yonder.com> | 2014-08-27 22:57:27 +0200 |
---|---|---|
committer | bdanielby <Bruno.Daniel@blue-yonder.com> | 2014-08-27 22:57:27 +0200 |
commit | 131a5e074e72ac57ba6ed0e1ca0661c864b83ff4 (patch) | |
tree | 9b84a7ac063e620c96f011cb98d0785e4c5235fe | |
parent | 2995e293950edc39b0466d1274affc1136c066c8 (diff) | |
parent | 0fd92f1790840130cebd8bbbc77d2811a9be3c91 (diff) | |
download | pylint-131a5e074e72ac57ba6ed0e1ca0661c864b83ff4.tar.gz |
Merged logilab/pylint into default
-rw-r--r-- | ChangeLog | 8 | ||||
-rw-r--r-- | doc/extensions.rst | 76 | ||||
-rw-r--r-- | doc/index.rst | 1 | ||||
-rw-r--r-- | extensions/__init__.py | 0 | ||||
-rw-r--r-- | extensions/check_docs.py | 206 | ||||
-rw-r--r-- | test/extensions/test_check_docs.py | 191 |
6 files changed, 481 insertions, 1 deletions
@@ -43,9 +43,15 @@ ChangeLog for Pylint * Properly handle unicode format strings for Python 2. Closes issue #296. + * Added new module 'extensions' for optional checkers with the test + directory 'test/extensions' and documentation file 'doc/extensions.rst'. + + * Added new checker 'extensions.check_docs' that verifies Sphinx parameter + documention + * Don't emit 'import-error' if an import was protected by a try-except, which excepted ImportError. - + * Fix an 'unused-import' false positive, when the error was emitted for all the members imported with 'from import' form. Closes issue #304. diff --git a/doc/extensions.rst b/doc/extensions.rst new file mode 100644 index 0000000..1e80097 --- /dev/null +++ b/doc/extensions.rst @@ -0,0 +1,76 @@ + +Optional Pylint checkers in the extensions module +================================================= + +Sphinx parameter documentation checker +-------------------------------------- + +If you're using Sphinx to document your code, this optional component might +be useful for you. You can activate it by adding the line:: + + load-plugins=pylint.extensions.check_docs + +to the ``MASTER`` section of your ``.pylintrc``. + +This checker verifies that all function, method, and constructor parameters are +mentioned in the Sphinx ``param`` and ``type`` parts of the docstring:: + + def function_foo(x, y, z): + '''function foo ... + + :param x: bla x + :type x: int + + :param y: bla y + :type y: float + + :param int z: bla z + + :return: sum + :rtype: float + ''' + return x + y + z + +You'll be notified of **missing parameter documentation** but also of +**naming inconsistencies** between the signature and the documentation which +often arise when parameters are renamed automatically in the code, but not in the +documentation. + +By convention, constructor parameters are documented in the class docstring. +(``__init__`` and ``__new__`` methods are considered constructors.):: + + class ClassFoo(object): + '''docstring foo + + :param float x: bla x + + :param y: bla y + :type y: int + ''' + def __init__(self, x, y): + pass + +In some cases, having to document all parameters is a nuisance, for instance if +many of your functions or methods just follow a **common interface**. To remove +this burden, the checker accepts missing parameter documentation if one of the +following phrases is found in the docstring: + +* For the other parameters, see +* For the parameters, see + +(with arbitrary whitespace between the words). Please add a link to the +docstring defining the interface, e.g. a superclass method, after "see":: + + def callback(x, y, z): + '''callback ... + + :param x: bla x + :type x: int + + For the other parameters, see + :class:`MyFrameworkUsingAndDefiningCallback` + ''' + return x + y + z + +Naming inconsistencies in existing ``param`` and ``type`` documentations are +still detected. diff --git a/doc/index.rst b/doc/index.rst index 7b8725c..57280e5 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -13,6 +13,7 @@ https://bitbucket.org/logilab/pylint output message-control features + extensions options extend ide-integration diff --git a/extensions/__init__.py b/extensions/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/extensions/__init__.py diff --git a/extensions/check_docs.py b/extensions/check_docs.py new file mode 100644 index 0000000..2319ac2 --- /dev/null +++ b/extensions/check_docs.py @@ -0,0 +1,206 @@ +"""Pylint plugin for Sphinx parameter documentation checking +""" +from __future__ import print_function, division, absolute_import + +import re + +from pylint.interfaces import IAstroidChecker +from pylint.checkers import BaseChecker +import astroid.scoped_nodes + + +class SphinxDocChecker(BaseChecker): + """Checker for Sphinx documentation parameters + + * Check that all function, method and constructor parameters are mentioned + in the Sphinx params and types part of the docstring. By convention, + constructor parameters are documented in the class docstring. + * Check that there are no naming inconsistencies between the signature and + the documentation, i.e. also report documented parameters that are missing + in the signature. This is important to find cases where parameters are + renamed only in the code, not in the documentation. + + Activate this checker by adding the line:: + + load-plugins=pylint.extensions.check_docs + + to the ``MASTER`` section of your ``.pylintrc``. + + :param linter: linter object + :type linter: :class:`pylint.lint.PyLinter` + """ + __implements__ = IAstroidChecker + + name = 'Sphinx doc checks' + msgs = { + 'W9003': ('"%s" missing or differing in Sphinx params', + 'missing-sphinx-param', + 'Please add Sphinx param declarations for all arguments.'), + 'W9004': ('"%s" missing or differing in Sphinx types', + 'missing-sphinx-type', + 'Please add Sphinx type declarations for all arguments.'), + } + + options = () + + priority = -2 + + def __init__(self, linter=None): + BaseChecker.__init__(self, linter) + + def visit_function(self, node): + """Called for function and method definitions (def). + + :param node: Node for a function or method definition in the AST + :type node: :class:`astroid.scoped_nodes.Function` + """ + self.check_arguments_in_docstring(node, node.doc, node.args) + + re_for_parameters_see = re.compile(r""" + For\s+the\s+(other)?\s*parameters\s*,\s+see + """, re.X | re.S) + + re_prefix_of_func_name = re.compile(r""" + .* # part before final dot + \. # final dot + """, re.X | re.S) + + re_sphinx_param_in_docstring = re.compile(r""" + :param # Sphinx keyword + \s+ # whitespace + + (?: # optional type declaration + (\w+) + \s+ + )? + + (\w+) # Parameter name + \s* # whitespace + : # final colon + """, re.X | re.S) + + re_sphinx_type_in_docstring = re.compile(r""" + :type # Sphinx keyword + \s+ # whitespace + (\w+) # Parameter name + \s* # whitespace + : # final colon + """, re.X | re.S) + + not_needed_param_in_docstring = set(['self', 'cls']) + + def check_arguments_in_docstring(self, node, doc, arguments_node): + """Check that all arguments in a function, method or class constructor + on the one hand and the arguments mentioned in the Sphinx tags 'param' + and 'type' on the other hand are consistent with each other. + + * Undocumented parameters except 'self' are noticed. + * Undocumented parameter types except for 'self' and the ``*<args>`` + and ``**<kwargs>`` parameters are noticed. + * Parameters mentioned in the Sphinx documentation that don't or no + longer exist in the function parameter list are noticed. + * If there is a Sphinx link like ``:meth:...`` or ``:func:...`` to a + function carrying the same name as the current function, missing + parameter documentations are tolerated, but the existing parameters + mentioned in the documentation are checked. + + :param node: Node for a function, method or class definition in the AST. + :type node: :class:`astroid.scoped_nodes.Function` or + :class:`astroid.scoped_nodes.Class` + + :param doc: Docstring for the function, method or class. + :type doc: str + + :param arguments_node: Arguments node for the function, method or + class constructor. + :type arguments_node: :class:`astroid.scoped_nodes.Arguments` + """ + # Tolerate missing param or type declarations if there is a link to + # another method carrying the same name. + if node.doc is None: + return + + tolerate_missing_params = False + if self.re_for_parameters_see.search(doc) is not None: + tolerate_missing_params = True + + # Collect the function arguments. + expected_argument_names = [arg.name for arg in arguments_node.args] + not_needed_type_in_docstring = ( + self.not_needed_param_in_docstring.copy()) + + if arguments_node.vararg is not None: + expected_argument_names.append(arguments_node.vararg) + not_needed_type_in_docstring.add(arguments_node.vararg) + if arguments_node.kwarg is not None: + expected_argument_names.append(arguments_node.kwarg) + not_needed_type_in_docstring.add(arguments_node.kwarg) + + def compare_args(found_argument_names, message_id, not_needed_names): + """Compare the found argument names with the expected ones and + generate a message if there are inconsistencies. + + :param list found_argument_names: argument names found in the + docstring + + :param str message_id: pylint message id + + :param not_needed_names: names that may be omitted + :type not_needed_names: set of str + """ + if not tolerate_missing_params: + missing_or_differing_argument_names = ( + (set(expected_argument_names) ^ set(found_argument_names)) + - not_needed_names) + else: + missing_or_differing_argument_names = ( + (set(found_argument_names) - set(expected_argument_names)) + - not_needed_names) + + if missing_or_differing_argument_names: + self.add_message( + message_id, + args=(', '.join( + sorted(missing_or_differing_argument_names)),), + node=node) + + # Sphinx param declarations + found_argument_names = [] + for match in re.finditer(self.re_sphinx_param_in_docstring, doc): + name = match.group(2) + found_argument_names.append(name) + if match.group(1) is not None: + not_needed_type_in_docstring.add(name) + compare_args(found_argument_names, 'missing-sphinx-param', + self.not_needed_param_in_docstring) + + # Sphinx type declarations + found_argument_names = re.findall(self.re_sphinx_type_in_docstring, doc) + compare_args(found_argument_names, 'missing-sphinx-type', + not_needed_type_in_docstring) + + constructor_names = set(["__init__", "__new__"]) + + def visit_class(self, node): + """Called for class definitions. + + :param node: Node for a class definition in the AST + :type node: :class:`astroid.scoped_nodes.Class` + """ + for body_item in node.body: + if (isinstance(body_item, astroid.scoped_nodes.Function) + and hasattr(body_item, 'name')): + if body_item.name in self.constructor_names: + self.check_arguments_in_docstring( + node, node.doc, body_item.args) + else: + self.visit_function(body_item) + + +def register(linter): + """Required method to auto register this checker. + + :param linter: Main interface object for Pylint plugins + :type linter: Pylint object + """ + linter.register_checker(SphinxDocChecker(linter)) diff --git a/test/extensions/test_check_docs.py b/test/extensions/test_check_docs.py new file mode 100644 index 0000000..59f5d7e --- /dev/null +++ b/test/extensions/test_check_docs.py @@ -0,0 +1,191 @@ +"""Unit tests for the pylint checkers in :mod:`pylint.extensions.check_docs`, +in particular the Sphinx parameter documentation checker `SphinxDocChecker` +""" +from __future__ import division, print_function, absolute_import + +import unittest + +from astroid import test_utils +from pylint.testutils import CheckerTestCase, Message + +from pylint.extensions.check_docs import SphinxDocChecker + + +class SpinxDocCheckerTest(CheckerTestCase): + """Tests for pylint_plugin.SphinxDocChecker""" + CHECKER_CLASS = SphinxDocChecker + + def test_missing_func_params_in_docstring(self): + """Example of a function with missing parameter documentation in the + docstring + """ + node = test_utils.extract_node(""" + def function_foo(x, y): + '''docstring ... + + missing parameter documentation''' + pass + """) + with self.assertAddsMessages( + Message( + msg_id='missing-sphinx-param', + node=node, + args=('x, y',)), + Message( + msg_id='missing-sphinx-type', + node=node, + args=('x, y',)) + ): + self.checker.visit_function(node) + + def test_missing_method_params_in_docstring(self): + """Example of a class method with missing parameter documentation in + the docstring + """ + node = test_utils.extract_node(""" + class Foo(object): + def method_foo(self, x, y): + '''docstring ... + + missing parameter documentation''' + pass + """) + method_node = node.body[0] + with self.assertAddsMessages( + Message( + msg_id='missing-sphinx-param', + node=method_node, + args=('x, y',)), + Message( + msg_id='missing-sphinx-type', + node=method_node, + args=('x, y',)) + ): + self.checker.visit_class(node) + + def test_existing_func_params_in_docstring(self): + """Example of a function with correctly documented parameters and + return values + """ + node = test_utils.extract_node(""" + def function_foo(xarg, yarg, zarg): + '''function foo ... + + :param xarg: bla xarg + :type xarg: int + + :param yarg: bla yarg + :type yarg: float + + :param int zarg: bla zarg + + :return: sum + :rtype: float + ''' + return xarg + yarg + """) + with self.assertNoMessages(): + self.checker.visit_function(node) + + def test_wrong_name_of_func_params_in_docstring(self): + """Example of functions with inconsistent parameter names in the + signature and in the documentation + """ + node = test_utils.extract_node(""" + def function_foo(xarg, yarg, zarg): + '''function foo ... + + :param xarg1: bla xarg + :type xarg: int + + :param yarg: bla yarg + :type yarg1: float + + :param str zarg1: bla zarg + ''' + return xarg + yarg + """) + with self.assertAddsMessages( + Message( + msg_id='missing-sphinx-param', + node=node, + args=('xarg, xarg1, zarg, zarg1',)), + Message( + msg_id='missing-sphinx-type', + node=node, + args=('yarg, yarg1, zarg',)), + ): + self.checker.visit_function(node) + + node = test_utils.extract_node(""" + def function_foo(xarg, yarg): + '''function foo ... + + :param yarg1: bla yarg + :type yarg1: float + + For the other parameters, see bla. + ''' + return xarg + yarg + """) + with self.assertAddsMessages( + Message( + msg_id='missing-sphinx-param', + node=node, + args=('yarg1',)), + Message( + msg_id='missing-sphinx-type', + node=node, + args=('yarg1',)) + ): + self.checker.visit_function(node) + + def test_see_sentence_for_func_params_in_docstring(self): + """Example for the usage of "For the other parameters, see" to avoid + too many repetitions, e.g. in functions or methods adhering to a + given interface + """ + node = test_utils.extract_node(""" + def function_foo(xarg, yarg): + '''function foo ... + + :param yarg: bla yarg + :type yarg: float + + For the other parameters, see :func:`bla` + ''' + return xarg + yarg + """) + with self.assertNoMessages(): + self.checker.visit_function(node) + + def test_constr_params_in_class(self): + """Example of a class with missing constructor parameter documentation + + Everything is completely analogous to functions. + """ + node = test_utils.extract_node(""" + class ClassFoo(object): + '''docstring foo + + missing constructor parameter documentation''' + + def __init__(self, x, y): + pass + + """) + with self.assertAddsMessages( + Message( + msg_id='missing-sphinx-param', + node=node, + args=('x, y',)), + Message( + msg_id='missing-sphinx-type', + node=node, + args=('x, y',)) + ): + self.checker.visit_class(node) + + +if __name__ == '__main__': + unittest.main() |