Repository: chainer/chainer

Issue:
Make a rule where to put a docstring for an __init__ method
Some classes in Chainer put docstrings for `__init__` methods into class docstrings. https://github.com/pfnet/chainer/blob/master/chainer/links/connection/linear.py

Some classes in Chainer put them into `__init__`'s own docstring. https://github.com/pfnet/chainer/blob/master/chainer/computational_graph.py

We should make a clear rule on which to prefer. Sphyinx's google style example says both are ok but should not be mixed. http://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html


Discussion:
I prefer the former one. And we can check if docstrings follow this rule with checking `AClass.__init__.__doc__`.

Resolving patch:
diff --git a/chainer/computational_graph.py b/chainer/computational_graph.py
--- a/chainer/computational_graph.py
+++ b/chainer/computational_graph.py
@@ -10,16 +10,13 @@ class DotNode(object):
     This class represents a node of computational graph,
     with some utilities for dot language.
 
+    Args:
+        node: :class: `Variable` object or :class: `Function` object.
+        attribute (dict): Attributes for the node.
+
     """
 
     def __init__(self, node, attribute=None):
-        """Initializes DotNode.
-
-        Args:
-            node: :class: `Variable` object or :class: `Function` object.
-            attribute (dict): Attributes for the node.
-
-        """
         assert isinstance(node, (variable.Variable, function.Function))
         self.node = node
         self.id_ = id(node)
@@ -53,23 +50,20 @@ class ComputationalGraph(object):
 
       We assume that the computational graph is directed and acyclic.
 
+    Args:
+        nodes (list): List of nodes. Each node is either
+             :class:`Variable` object or :class:`Function` object.
+        edges (list): List of edges. Each edge consists of pair of nodes.
+        variable_style (dict): Dot node style for variable.
+        function_style (dict): Dot node style for function.
+        rankdir (str): Direction of the graph that must be
+            TB (top to bottom), BT (bottom to top), LR (left to right)
+            or RL (right to left).
+
     """
 
     def __init__(self, nodes, edges, variable_style=None, function_style=None,
                  rankdir='TB'):
-        """Initializes computational graph.
-
-        Args:
-            nodes (list): List of nodes. Each node is either
-                 :class:`Variable` object or :class:`Function` object.
-            edges (list): List of edges. Each edge consists of pair of nodes.
-            variable_style (dict): Dot node style for variable.
-            function_style (dict): Dot node style for function.
-            rankdir (str): Direction of the graph that must be
-                TB (top to bottom), BT (bottom to top), LR (left to right)
-                or RL (right to left).
-
-        """
         self.nodes = nodes
         self.edges = edges
         self.variable_style = variable_style
diff --git a/chainer/links/model/vision/resnet.py b/chainer/links/model/vision/resnet.py
--- a/chainer/links/model/vision/resnet.py
+++ b/chainer/links/model/vision/resnet.py
@@ -1,5 +1,5 @@
 from __future__ import print_function
-from collections import OrderedDict
+import collections
 import os
 
 import numpy
@@ -94,7 +94,7 @@ def __init__(self, pretrained_model='auto'):
                 'ResNet-50-model.npz', 'ResNet-50-model.caffemodel', self)
         elif pretrained_model:
             npz.load_npz(pretrained_model, self)
-        self.functions = OrderedDict([
+        self.functions = collections.OrderedDict([
             ('conv1', [self.conv1, self.bn1, relu]),
             ('pool1', [lambda x: max_pooling_2d(x, ksize=3, stride=2)]),
             ('res2', [self.res2]),
diff --git a/chainer/links/model/vision/vgg.py b/chainer/links/model/vision/vgg.py
--- a/chainer/links/model/vision/vgg.py
+++ b/chainer/links/model/vision/vgg.py
@@ -1,5 +1,5 @@
 from __future__ import print_function
-from collections import OrderedDict
+import collections
 import os
 
 import numpy
@@ -110,7 +110,7 @@ def __init__(self, pretrained_model='auto'):
         elif pretrained_model:
             npz.load_npz(pretrained_model, self)
 
-        self.functions = OrderedDict([
+        self.functions = collections.OrderedDict([
             ('conv1_1', [self.conv1_1, relu]),
             ('conv1_2', [self.conv1_2, relu]),
             ('pool1', [_max_pooling_2d]),


Test patch:
diff --git a/tests/chainer_tests/test_init_docstring.py b/tests/chainer_tests/test_init_docstring.py
new file mode 100644
--- /dev/null
+++ b/tests/chainer_tests/test_init_docstring.py
@@ -0,0 +1,63 @@
+import inspect
+import pkgutil
+import unittest
+
+import chainer
+from chainer import testing
+
+
+def get_init_doc(klass):
+    for attr in inspect.classify_class_attrs(klass):
+        if attr.name == '__init__':
+            if attr.defining_class is klass:
+                return attr.object.__doc__
+            else:
+                # Ignore __init__ method inherited from a super class
+                return None
+    return None
+
+
+class TestInitDocstring(unittest.TestCase):
+
+    def check_init_docstring(self, mod, errors):
+        for name, value in inspect.getmembers(mod):
+            if not inspect.isclass(value):
+                continue
+            init_doc = get_init_doc(value)
+            if init_doc == object.__init__.__doc__:
+                # Ignore doc string inherited from `object`
+                continue
+
+            if init_doc is not None:
+                # Do not permit to write docstring in `__init__`
+                errors.append((mod, value, init_doc))
+
+    def test_init_docstring_empty(self):
+        errors = []
+        root = chainer.__file__
+        for importer, modname, _ in pkgutil.walk_packages(root):
+            if 'chainer' not in modname:
+                # Skip tests
+                continue
+
+            loader = importer.find_module(modname)
+            try:
+                mod = loader.load_module(modname)
+            except Exception as e:
+                print('Failed to load module accidentally: %s' %
+                      loader.fullname)
+                print(e)
+                print('')
+                continue
+
+            self.check_init_docstring(mod, errors)
+
+        if errors:
+            msg = ''
+            for mod, value, init_doc in errors:
+                msg += '{}.{} has __init__.__doc__:\n{}\n\n'.format(
+                    mod.__name__, value, init_doc)
+            self.fail(msg)
+
+
+testing.run_module(__name__, __file__)