summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBruno Daniel <bruno.daniel@blue-yonder.com>2015-05-08 16:39:00 +0200
committerBruno Daniel <bruno.daniel@blue-yonder.com>2015-05-08 16:39:00 +0200
commit087ef5378b0d392f172747d7130512e25d3844ca (patch)
treeb127e203c276261a25e07c33d2e2d25316753f3e
parentc275f0228ff24a1482d45b7b93393cae450f344e (diff)
parent63997ac37b22c3b7e138cea2127a7560d2aeb5e0 (diff)
downloadpylint-087ef5378b0d392f172747d7130512e25d3844ca.tar.gz
merge
-rw-r--r--ChangeLog6
-rw-r--r--doc/extensions.rst76
-rw-r--r--doc/index.rst1
-rw-r--r--extensions/__init__.py0
-rw-r--r--extensions/check_docs.py206
-rw-r--r--test/extensions/test_check_docs.py191
6 files changed, 480 insertions, 0 deletions
diff --git a/ChangeLog b/ChangeLog
index 687327d..444867d 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -275,6 +275,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' 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.
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 01f44c8..4489ade 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()