From 830a36a5687f95dc9ab601c0039ebf1ea1355f05 Mon Sep 17 00:00:00 2001 From: Bruno Daniel Date: Wed, 30 Jul 2014 18:31:54 +0200 Subject: * created new module pylint/extensions * added new optional checker pylint/check_docs.py containing SphinxDocChecker which checks the documentation of function, method, and constructor parameters * added a test for this checker pylint/test/test_check_docs.py --- extensions/__init__.py | 0 extensions/check_docs.py | 181 +++++++++++++++++++++++++++++++++++++++++++++++ test/test_check_docs.py | 180 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 361 insertions(+) create mode 100644 extensions/__init__.py create mode 100644 extensions/check_docs.py create mode 100644 test/test_check_docs.py diff --git a/extensions/__init__.py b/extensions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/extensions/check_docs.py b/extensions/check_docs.py new file mode 100644 index 0000000..196aa81 --- /dev/null +++ b/extensions/check_docs.py @@ -0,0 +1,181 @@ +"""Pylint plugin for Sphinx parameter documentation checking +""" +from __future__ import print_function, division + +import re +# import pprint + +from pylint.interfaces import IAstroidChecker +from pylint.checkers import BaseChecker +from pylint.checkers.typecheck import TypeChecker +from pylint.checkers.utils import check_messages +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. + """ + __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_mentioned_in_docstring( + node, node.doc, node.name, 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 + (\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_mentioned_in_docstring( + self, node, doc, name, 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 ``*`` + and ``**`` 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 name: Name of the function, method or class. + :type name: 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) + + # Compare the function arguments with the ones found in the Sphinx + # docstring. + for message_id, pattern, not_needed in [ + ('W9003', self.re_sphinx_param_in_docstring, + self.not_needed_param_in_docstring), + ('W9004', self.re_sphinx_type_in_docstring, + not_needed_type_in_docstring), + ]: + found_argument_names = re.findall(pattern, doc) + if not tolerate_missing_params: + missing_or_differing_argument_names = ( + (set(expected_argument_names) ^ set(found_argument_names)) + - not_needed) + else: + missing_or_differing_argument_names = ( + (set(found_argument_names) - set(expected_argument_names)) + - not_needed) + + if missing_or_differing_argument_names: + self.add_message( + message_id, + args=(', '.join( + sorted(missing_or_differing_argument_names)),), + node=node) + + 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_mentioned_in_docstring( + node, node.doc, node.name, 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/test_check_docs.py b/test/test_check_docs.py new file mode 100644 index 0000000..eafec10 --- /dev/null +++ b/test/test_check_docs.py @@ -0,0 +1,180 @@ +"""Unittest for our pylint plugin.""" +from __future__ import division, print_function + +import unittest + +from astroid import test_utils +from pylint.testutils import CheckerTestCase, Message, set_config + +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='W9003', + node=node, + args=('x, y',)), + Message( + msg_id='W9004', + 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='W9003', + node=method_node, + args=('x, y',)), + Message( + msg_id='W9004', + 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): + '''function foo ... + + :param xarg: bla xarg + :type xarg: int + + :param yarg: bla yarg + :type yarg: float + + :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): + '''function foo ... + + :param xarg1: bla xarg + :type xarg: int + + :param yarg: bla yarg + :type yarg1: float + ''' + return xarg + yarg + """) + with self.assertAddsMessages( + Message( + msg_id='W9003', + node=node, + args=('xarg, xarg1',)), + Message( + msg_id='W9004', + node=node, + args=('yarg, yarg1',))): + 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='W9003', + node=node, + args=('yarg1',)), + Message( + msg_id='W9004', + 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='W9003', + node=node, + args=('x, y',)), + Message( + msg_id='W9004', + node=node, + args=('x, y',))): + self.checker.visit_class(node) + + +if __name__ == '__main__': + unittest.main() -- cgit v1.2.1 From 66cb2bd78ac7ee1226bc60748c3f1482906e6f67 Mon Sep 17 00:00:00 2001 From: Bruno Daniel Date: Wed, 30 Jul 2014 18:33:43 +0200 Subject: documentation about how to activate the new checker pylint.extensions.check_docs --- extensions/check_docs.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/extensions/check_docs.py b/extensions/check_docs.py index 196aa81..53b1447 100644 --- a/extensions/check_docs.py +++ b/extensions/check_docs.py @@ -22,6 +22,12 @@ class SphinxDocChecker(BaseChecker): 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``. """ __implements__ = IAstroidChecker -- cgit v1.2.1 From 6b3a9324ed84914daaa6f76df3bc00e2a4a00b71 Mon Sep 17 00:00:00 2001 From: Bruno Daniel Date: Thu, 31 Jul 2014 16:04:10 +0200 Subject: removed superfluous comments and imports; 80-characters limit; removed trailing whitespace --- extensions/check_docs.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/extensions/check_docs.py b/extensions/check_docs.py index 53b1447..1b51f40 100644 --- a/extensions/check_docs.py +++ b/extensions/check_docs.py @@ -3,20 +3,17 @@ from __future__ import print_function, division import re -# import pprint from pylint.interfaces import IAstroidChecker from pylint.checkers import BaseChecker -from pylint.checkers.typecheck import TypeChecker -from pylint.checkers.utils import check_messages 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, + * 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 @@ -24,7 +21,7 @@ class SphinxDocChecker(BaseChecker): 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``. @@ -177,7 +174,7 @@ class SphinxDocChecker(BaseChecker): else: self.visit_function(body_item) - + def register(linter): """Required method to auto register this checker. -- cgit v1.2.1 From e48432a31b54fa15922c163736e2533b8b7254e5 Mon Sep 17 00:00:00 2001 From: Bruno Daniel Date: Thu, 31 Jul 2014 16:11:06 +0200 Subject: * used symbolic names instead of "W9003" etc. * 80-character limit * removed superfluous imports * better module docstring for the test module * removed trailing whitespace --- extensions/check_docs.py | 4 ++-- test/test_check_docs.py | 38 ++++++++++++++++++++------------------ 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/extensions/check_docs.py b/extensions/check_docs.py index 1b51f40..4be0cea 100644 --- a/extensions/check_docs.py +++ b/extensions/check_docs.py @@ -135,9 +135,9 @@ class SphinxDocChecker(BaseChecker): # Compare the function arguments with the ones found in the Sphinx # docstring. for message_id, pattern, not_needed in [ - ('W9003', self.re_sphinx_param_in_docstring, + ('missing-sphinx-param', self.re_sphinx_param_in_docstring, self.not_needed_param_in_docstring), - ('W9004', self.re_sphinx_type_in_docstring, + ('missing-sphinx-type', self.re_sphinx_type_in_docstring, not_needed_type_in_docstring), ]: found_argument_names = re.findall(pattern, doc) diff --git a/test/test_check_docs.py b/test/test_check_docs.py index eafec10..fc6c61e 100644 --- a/test/test_check_docs.py +++ b/test/test_check_docs.py @@ -1,10 +1,12 @@ -"""Unittest for our pylint plugin.""" +"""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 import unittest from astroid import test_utils -from pylint.testutils import CheckerTestCase, Message, set_config +from pylint.testutils import CheckerTestCase, Message from pylint.extensions.check_docs import SphinxDocChecker @@ -26,18 +28,18 @@ class SpinxDocCheckerTest(CheckerTestCase): """) with self.assertAddsMessages( Message( - msg_id='W9003', + msg_id='missing-sphinx-param', node=node, args=('x, y',)), Message( - msg_id='W9004', + 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 + """Example of a class method with missing parameter documentation in + the docstring """ node = test_utils.extract_node(""" class Foo(object): @@ -50,18 +52,18 @@ class SpinxDocCheckerTest(CheckerTestCase): method_node = node.body[0] with self.assertAddsMessages( Message( - msg_id='W9003', + msg_id='missing-sphinx-param', node=method_node, args=('x, y',)), Message( - msg_id='W9004', + 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 + """Example of a function with correctly documented parameters and + return values """ node = test_utils.extract_node(""" def function_foo(xarg, yarg): @@ -99,11 +101,11 @@ class SpinxDocCheckerTest(CheckerTestCase): """) with self.assertAddsMessages( Message( - msg_id='W9003', + msg_id='missing-sphinx-param', node=node, args=('xarg, xarg1',)), Message( - msg_id='W9004', + msg_id='missing-sphinx-type', node=node, args=('yarg, yarg1',))): self.checker.visit_function(node) @@ -121,19 +123,19 @@ class SpinxDocCheckerTest(CheckerTestCase): """) with self.assertAddsMessages( Message( - msg_id='W9003', + msg_id='missing-sphinx-param', node=node, args=('yarg1',)), Message( - msg_id='W9004', + 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 + too many repetitions, e.g. in functions or methods adhering to a + given interface """ node = test_utils.extract_node(""" def function_foo(xarg, yarg): @@ -166,11 +168,11 @@ class SpinxDocCheckerTest(CheckerTestCase): """) with self.assertAddsMessages( Message( - msg_id='W9003', + msg_id='missing-sphinx-param', node=node, args=('x, y',)), Message( - msg_id='W9004', + msg_id='missing-sphinx-type', node=node, args=('x, y',))): self.checker.visit_class(node) -- cgit v1.2.1 From 0d71e4f0c1fac4ba8e293de0c5fd8ca877b1ae50 Mon Sep 17 00:00:00 2001 From: Bruno Daniel Date: Thu, 31 Jul 2014 16:27:26 +0200 Subject: used pylint on my own code and fixed all warnings --- extensions/check_docs.py | 22 ++++++------- test/test_check_docs.py | 85 +++++++++++++++++++++++++----------------------- 2 files changed, 55 insertions(+), 52 deletions(-) diff --git a/extensions/check_docs.py b/extensions/check_docs.py index 4be0cea..c3437bb 100644 --- a/extensions/check_docs.py +++ b/extensions/check_docs.py @@ -25,6 +25,9 @@ class SphinxDocChecker(BaseChecker): 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 @@ -51,8 +54,7 @@ class SphinxDocChecker(BaseChecker): :param node: Node for a function or method definition in the AST :type node: :class:`astroid.scoped_nodes.Function` """ - self.check_arguments_mentioned_in_docstring( - node, node.doc, node.name, node.args) + self.check_arguments_mentioned_in_docstring(node, node.doc, node.args) re_for_parameters_see = re.compile(r""" For\s+the\s+(other)?\s*parameters\s*,\s+see @@ -81,8 +83,7 @@ class SphinxDocChecker(BaseChecker): not_needed_param_in_docstring = set(['self', 'cls']) - def check_arguments_mentioned_in_docstring( - self, node, doc, name, arguments_node): + def check_arguments_mentioned_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. @@ -104,9 +105,6 @@ class SphinxDocChecker(BaseChecker): :param doc: Docstring for the function, method or class. :type doc: str - :param name: Name of the function, method or class. - :type name: str - :param arguments_node: Arguments node for the function, method or class constructor. :type arguments_node: :class:`astroid.scoped_nodes.Arguments` @@ -135,10 +133,10 @@ class SphinxDocChecker(BaseChecker): # Compare the function arguments with the ones found in the Sphinx # docstring. for message_id, pattern, not_needed in [ - ('missing-sphinx-param', self.re_sphinx_param_in_docstring, - self.not_needed_param_in_docstring), - ('missing-sphinx-type', self.re_sphinx_type_in_docstring, - not_needed_type_in_docstring), + ('missing-sphinx-param', self.re_sphinx_param_in_docstring, + self.not_needed_param_in_docstring), + ('missing-sphinx-type', self.re_sphinx_type_in_docstring, + not_needed_type_in_docstring), ]: found_argument_names = re.findall(pattern, doc) if not tolerate_missing_params: @@ -170,7 +168,7 @@ class SphinxDocChecker(BaseChecker): and hasattr(body_item, 'name')): if body_item.name in self.constructor_names: self.check_arguments_mentioned_in_docstring( - node, node.doc, node.name, body_item.args) + node, node.doc, body_item.args) else: self.visit_function(body_item) diff --git a/test/test_check_docs.py b/test/test_check_docs.py index fc6c61e..4da4f3c 100644 --- a/test/test_check_docs.py +++ b/test/test_check_docs.py @@ -27,14 +27,15 @@ class SpinxDocCheckerTest(CheckerTestCase): 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',))): + 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): @@ -51,14 +52,15 @@ class SpinxDocCheckerTest(CheckerTestCase): """) 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',))): + 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): @@ -100,14 +102,15 @@ class SpinxDocCheckerTest(CheckerTestCase): return xarg + yarg """) with self.assertAddsMessages( - Message( - msg_id='missing-sphinx-param', - node=node, - args=('xarg, xarg1',)), - Message( - msg_id='missing-sphinx-type', - node=node, - args=('yarg, yarg1',))): + Message( + msg_id='missing-sphinx-param', + node=node, + args=('xarg, xarg1',)), + Message( + msg_id='missing-sphinx-type', + node=node, + args=('yarg, yarg1',)) + ): self.checker.visit_function(node) node = test_utils.extract_node(""" @@ -122,14 +125,15 @@ class SpinxDocCheckerTest(CheckerTestCase): 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',))): + 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): @@ -167,14 +171,15 @@ class SpinxDocCheckerTest(CheckerTestCase): """) 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',))): + 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) -- cgit v1.2.1 From e74c53ae2da0dae48f183d59b3c4407dbf4ae5d8 Mon Sep 17 00:00:00 2001 From: Bruno Daniel Date: Thu, 31 Jul 2014 16:29:55 +0200 Subject: moved test_check_docs.py to the new directory pylint/test/extensions --- test/extensions/test_check_docs.py | 187 +++++++++++++++++++++++++++++++++++++ test/test_check_docs.py | 187 ------------------------------------- 2 files changed, 187 insertions(+), 187 deletions(-) create mode 100644 test/extensions/test_check_docs.py delete mode 100644 test/test_check_docs.py diff --git a/test/extensions/test_check_docs.py b/test/extensions/test_check_docs.py new file mode 100644 index 0000000..4da4f3c --- /dev/null +++ b/test/extensions/test_check_docs.py @@ -0,0 +1,187 @@ +"""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 + +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): + '''function foo ... + + :param xarg: bla xarg + :type xarg: int + + :param yarg: bla yarg + :type yarg: float + + :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): + '''function foo ... + + :param xarg1: bla xarg + :type xarg: int + + :param yarg: bla yarg + :type yarg1: float + ''' + return xarg + yarg + """) + with self.assertAddsMessages( + Message( + msg_id='missing-sphinx-param', + node=node, + args=('xarg, xarg1',)), + Message( + msg_id='missing-sphinx-type', + node=node, + args=('yarg, yarg1',)) + ): + 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() diff --git a/test/test_check_docs.py b/test/test_check_docs.py deleted file mode 100644 index 4da4f3c..0000000 --- a/test/test_check_docs.py +++ /dev/null @@ -1,187 +0,0 @@ -"""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 - -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): - '''function foo ... - - :param xarg: bla xarg - :type xarg: int - - :param yarg: bla yarg - :type yarg: float - - :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): - '''function foo ... - - :param xarg1: bla xarg - :type xarg: int - - :param yarg: bla yarg - :type yarg1: float - ''' - return xarg + yarg - """) - with self.assertAddsMessages( - Message( - msg_id='missing-sphinx-param', - node=node, - args=('xarg, xarg1',)), - Message( - msg_id='missing-sphinx-type', - node=node, - args=('yarg, yarg1',)) - ): - 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() -- cgit v1.2.1 From 2575bb3b46353a5e596ce634afc34acb32be1310 Mon Sep 17 00:00:00 2001 From: Bruno Daniel Date: Fri, 8 Aug 2014 14:19:02 +0200 Subject: support for Sphinx type declarations like :param int mgid: --- extensions/check_docs.py | 31 ++++++++++++++++++++++--------- test/extensions/test_check_docs.py | 12 ++++++++---- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/extensions/check_docs.py b/extensions/check_docs.py index c3437bb..78c1be1 100644 --- a/extensions/check_docs.py +++ b/extensions/check_docs.py @@ -68,6 +68,12 @@ class SphinxDocChecker(BaseChecker): 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 @@ -130,15 +136,7 @@ class SphinxDocChecker(BaseChecker): expected_argument_names.append(arguments_node.kwarg) not_needed_type_in_docstring.add(arguments_node.kwarg) - # Compare the function arguments with the ones found in the Sphinx - # docstring. - for message_id, pattern, not_needed in [ - ('missing-sphinx-param', self.re_sphinx_param_in_docstring, - self.not_needed_param_in_docstring), - ('missing-sphinx-type', self.re_sphinx_type_in_docstring, - not_needed_type_in_docstring), - ]: - found_argument_names = re.findall(pattern, doc) + def compare_args(found_argument_names, message_id, not_needed): if not tolerate_missing_params: missing_or_differing_argument_names = ( (set(expected_argument_names) ^ set(found_argument_names)) @@ -155,6 +153,21 @@ class SphinxDocChecker(BaseChecker): 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): diff --git a/test/extensions/test_check_docs.py b/test/extensions/test_check_docs.py index 4da4f3c..cc2cbc4 100644 --- a/test/extensions/test_check_docs.py +++ b/test/extensions/test_check_docs.py @@ -68,7 +68,7 @@ class SpinxDocCheckerTest(CheckerTestCase): return values """ node = test_utils.extract_node(""" - def function_foo(xarg, yarg): + def function_foo(xarg, yarg, zarg): '''function foo ... :param xarg: bla xarg @@ -77,6 +77,8 @@ class SpinxDocCheckerTest(CheckerTestCase): :param yarg: bla yarg :type yarg: float + :param int zarg: bla zarg + :return: sum :rtype: float ''' @@ -90,7 +92,7 @@ class SpinxDocCheckerTest(CheckerTestCase): signature and in the documentation """ node = test_utils.extract_node(""" - def function_foo(xarg, yarg): + def function_foo(xarg, yarg, zarg): '''function foo ... :param xarg1: bla xarg @@ -98,6 +100,8 @@ class SpinxDocCheckerTest(CheckerTestCase): :param yarg: bla yarg :type yarg1: float + + :param str zarg1: bla zarg ''' return xarg + yarg """) @@ -105,11 +109,11 @@ class SpinxDocCheckerTest(CheckerTestCase): Message( msg_id='missing-sphinx-param', node=node, - args=('xarg, xarg1',)), + args=('xarg, xarg1, zarg, zarg1',)), Message( msg_id='missing-sphinx-type', node=node, - args=('yarg, yarg1',)) + args=('yarg, yarg1, zarg',)), ): self.checker.visit_function(node) -- cgit v1.2.1 From b9bdcc15b0d7aaf64c8265c2716738d6d42bc276 Mon Sep 17 00:00:00 2001 From: Bruno Daniel Date: Fri, 8 Aug 2014 14:27:56 +0200 Subject: Sphinx's mixed param-type declarations; clean up the code. --- extensions/check_docs.py | 25 ++++++++++++++++++------- test/extensions/test_check_docs.py | 2 +- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/extensions/check_docs.py b/extensions/check_docs.py index 78c1be1..38fc549 100644 --- a/extensions/check_docs.py +++ b/extensions/check_docs.py @@ -1,6 +1,6 @@ """Pylint plugin for Sphinx parameter documentation checking """ -from __future__ import print_function, division +from __future__ import print_function, division, absolute_import import re @@ -68,12 +68,12 @@ class SphinxDocChecker(BaseChecker): 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 @@ -136,15 +136,26 @@ class SphinxDocChecker(BaseChecker): 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): + 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) + - not_needed_names) else: missing_or_differing_argument_names = ( (set(found_argument_names) - set(expected_argument_names)) - - not_needed) + - not_needed_names) if missing_or_differing_argument_names: self.add_message( @@ -167,7 +178,7 @@ class SphinxDocChecker(BaseChecker): 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): diff --git a/test/extensions/test_check_docs.py b/test/extensions/test_check_docs.py index cc2cbc4..59f5d7e 100644 --- a/test/extensions/test_check_docs.py +++ b/test/extensions/test_check_docs.py @@ -1,7 +1,7 @@ """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 +from __future__ import division, print_function, absolute_import import unittest -- cgit v1.2.1 From 1ed31f94506b53219ae5d5e446bb71e4640f7775 Mon Sep 17 00:00:00 2001 From: Bruno Daniel Date: Fri, 8 Aug 2014 14:39:35 +0200 Subject: check_docs: shorter name for method --- extensions/check_docs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/check_docs.py b/extensions/check_docs.py index 38fc549..2319ac2 100644 --- a/extensions/check_docs.py +++ b/extensions/check_docs.py @@ -54,7 +54,7 @@ class SphinxDocChecker(BaseChecker): :param node: Node for a function or method definition in the AST :type node: :class:`astroid.scoped_nodes.Function` """ - self.check_arguments_mentioned_in_docstring(node, node.doc, node.args) + 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 @@ -89,7 +89,7 @@ class SphinxDocChecker(BaseChecker): not_needed_param_in_docstring = set(['self', 'cls']) - def check_arguments_mentioned_in_docstring(self, node, doc, arguments_node): + 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. @@ -191,7 +191,7 @@ class SphinxDocChecker(BaseChecker): if (isinstance(body_item, astroid.scoped_nodes.Function) and hasattr(body_item, 'name')): if body_item.name in self.constructor_names: - self.check_arguments_mentioned_in_docstring( + self.check_arguments_in_docstring( node, node.doc, body_item.args) else: self.visit_function(body_item) -- cgit v1.2.1 From 22eba2e97e081fbb53f9ebba3f0be894aff4b491 Mon Sep 17 00:00:00 2001 From: Bruno Daniel Date: Fri, 8 Aug 2014 14:44:32 +0200 Subject: changelog entry for the new extensions module and the new checker 'check_docs' --- ChangeLog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ChangeLog b/ChangeLog index bb79521..61bfdaa 100644 --- a/ChangeLog +++ b/ChangeLog @@ -38,6 +38,12 @@ 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'. + + * Added new checker 'extensions.check_docs' that verifies Sphinx parameter + documention + 2014-07-26 -- 1.3.0 * Allow hanging continued indentation for implicitly concatenated -- cgit v1.2.1 From 4ad7c89010566425b506ec882f2bf6286bcfe1dc Mon Sep 17 00:00:00 2001 From: Bruno Daniel Date: Fri, 8 Aug 2014 15:34:27 +0200 Subject: rst documentation file for the extensions module; extensive documentation for the new checker extensions.check_docs --- doc/index.rst | 1 + 1 file changed, 1 insertion(+) 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 -- cgit v1.2.1 From b2e519f8ea9d57259bb2bb0d29f2050bdd4d26ef Mon Sep 17 00:00:00 2001 From: Bruno Daniel Date: Fri, 8 Aug 2014 15:36:34 +0200 Subject: rst documentation file for the extensions module; extensive documentation for the new checker extensions.check_docs --- doc/extensions.rst | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 doc/extensions.rst 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. -- cgit v1.2.1