Repository: keras-team/keras-autodoc

Issue:
Docstring of super class doesn't show
@gabrieldemarmiesse 

I'm seeing an issue where if a docstring exists in the super class but not in the subclass method then it is not showing up at all

Discussion:

Patch:
diff --git a/keras_autodoc/autogen.py b/keras_autodoc/autogen.py
index 7f8e379..0318aca 100644
--- a/keras_autodoc/autogen.py
+++ b/keras_autodoc/autogen.py
@@ -1,6 +1,6 @@
 import shutil
 import pathlib
-from inspect import isclass, isfunction
+from inspect import isclass, isfunction, getdoc
 from typing import Dict, Union
 
 from .docstring import process_docstring
@@ -78,10 +78,10 @@ class DocumentationGenerator:
         signature = get_class_signature(cls, signature_override)
         signature = self.process_signature(signature)
         subblocks.append(utils.code_snippet(signature))
-        docstring = cls.__doc__
+        docstring = getdoc(cls)
         if docstring:
             subblocks.append(self.process_class_docstring(docstring, cls))
-        return '\n'.join(subblocks) + '\n----\n\n'
+        return '\n'.join(subblocks) + '\n\n----\n\n'
 
     def _render_method(self, method, signature_override=None):
         subblocks = []
@@ -90,11 +90,11 @@ class DocumentationGenerator:
         subblocks.append(f"### {method.__name__} method:\n")
         subblocks.append(utils.code_snippet(signature))
 
-        docstring = method.__doc__
+        docstring = getdoc(method)
         if docstring:
             docstring = self.process_method_docstring(docstring, method)
             subblocks.append(docstring)
-        return "\n\n".join(subblocks) + '\n----\n\n'
+        return "\n\n".join(subblocks) + '\n\n----\n\n'
 
     def _render_function(self, function, signature_override=None):
         subblocks = []
@@ -103,8 +103,8 @@ class DocumentationGenerator:
         subblocks.append(f"### {function.__name__} function:\n")
         subblocks.append(utils.code_snippet(signature))
 
-        docstring = function.__doc__
+        docstring = getdoc(function)
         if docstring:
             docstring = self.process_function_docstring(docstring, function)
             subblocks.append(docstring)
-        return "\n\n".join(subblocks) + '\n----\n\n'
+        return "\n\n".join(subblocks) + '\n\n----\n\n'
diff --git a/keras_autodoc/docstring.py b/keras_autodoc/docstring.py
index 5d71d4f..20cfc33 100644
--- a/keras_autodoc/docstring.py
+++ b/keras_autodoc/docstring.py
@@ -97,7 +97,7 @@ def get_code_blocks(docstring):
 
 def get_sections(docstring):
     # Format docstring lists.
-    section_regex = r"\n( +)# (.*)\n"
+    section_regex = r"\n( *)# (.*)\n"
     section_idx = re.search(section_regex, docstring)
     shift = 0
     sections = {}


Test patch:
diff --git a/tests/dummy_package/dummy_module.py b/tests/dummy_package/dummy_module.py
index 73e8f4b..58a028d 100644
--- a/tests/dummy_package/dummy_module.py
+++ b/tests/dummy_package/dummy_module.py
@@ -188,7 +188,9 @@ class ImageDataGenerator:
         interpolation_order: int, order to use for
             the spline interpolation. Higher is slower.
         dtype: Dtype to use for the generated arrays.
+
     # Examples
+
     Example of using `.flow(x, y)`:
     ```python
     (x_train, y_train), (x_test, y_test) = cifar10.load_data()
diff --git a/tests/dummy_package/expected.md b/tests/dummy_package/expected.md
index ba981ed..a764c49 100644
--- a/tests/dummy_package/expected.md
+++ b/tests/dummy_package/expected.md
@@ -69,7 +69,7 @@ __Output shape__
 nD tensor with shape: `(batch_size, ..., units)`.
 For instance, for a 2D input with shape `(batch_size, input_dim)`,
 the output would have shape `(batch_size, units)`.
-    
+
 ----
 
 <span style="float:right;">[[source]](www.dummy.com/my_project/tests/dummy_package/dummy_module.py#L113)</span>
@@ -157,8 +157,10 @@ __Arguments__
 - __interpolation_order__: int, order to use for
     the spline interpolation. Higher is slower.
 - __dtype__: Dtype to use for the generated arrays.
+
 __Examples__
 
+
 Example of using `.flow(x, y)`:
 ```python
 (x_train, y_train), (x_test, y_test) = cifar10.load_data()
@@ -278,7 +280,7 @@ model.fit_generator(
         validation_data=validation_generator,
         validation_steps=800)
 ```
-    
+
 ----
 
 ### flow method:
@@ -332,7 +334,7 @@ An `Iterator` yielding tuples of `(x, y)`
     of corresponding labels. If 'sample_weight' is not None,
     the yielded tuples are of the form `(x, y, sample_weight)`.
     If `y` is None, only the numpy array `x` is returned.
-    
+
 ----
 
 ### flow_from_directory method:
@@ -418,7 +420,7 @@ A `DirectoryIterator` yielding tuples of `(x, y)`
     where `x` is a numpy array containing a batch
     of images with shape `(batch_size, *target_size, channels)`
     and `y` is a numpy array of corresponding labels.
-    
+
 ----
 
 ### to_categorical function:
@@ -463,6 +465,6 @@ array([[ 1.,  0.,  0.],
        [ 0.,  0.,  1.],
        [ 1.,  0.,  0.]], dtype=float32)
 ```
-    
+
 ----
 
diff --git a/tests/test_autogen.py b/tests/test_autogen.py
index 327bd00..85dc003 100644
--- a/tests/test_autogen.py
+++ b/tests/test_autogen.py
@@ -8,156 +8,156 @@ from .dummy_package import dummy_module
 test_doc1 = {
     "doc": """Base class for recurrent layers.
 
-    # Arguments
-        cell: A RNN cell instance. A RNN cell is a class that has:
-            - a `call(input_at_t, states_at_t)` method, returning
-                `(output_at_t, states_at_t_plus_1)`. The call method of the
-                cell can also take the optional argument `constants`, see
-                section "Note on passing external constants" below.
-            - a `state_size` attribute. This can be a single integer
-                (single state) in which case it is
-                the size of the recurrent state
-                (which should be the same as the size of the cell output).
-                This can also be a list/tuple of integers
-                (one size per state). In this case, the first entry
-                (`state_size[0]`) should be the same as
-                the size of the cell output.
-            It is also possible for `cell` to be a list of RNN cell instances,
-            in which cases the cells get stacked on after the other in the RNN,
-            implementing an efficient stacked RNN.
-        return_sequences: Boolean. Whether to return the last output
-            in the output sequence, or the full sequence.
-        return_state: Boolean. Whether to return the last state
-            in addition to the output.
-        go_backwards: Boolean (default False).
-            If True, process the input sequence backwards and return the
-            reversed sequence.
-        stateful: Boolean (default False). If True, the last state
-            for each sample at index i in a batch will be used as initial
-            state for the sample of index i in the following batch.
-        unroll: Boolean (default False).
-            If True, the network will be unrolled,
-            else a symbolic loop will be used.
-            Unrolling can speed-up a RNN,
-            although it tends to be more memory-intensive.
-            Unrolling is only suitable for short sequences.
-        input_dim: dimensionality of the input (integer).
-            This argument (or alternatively,
-            the keyword argument `input_shape`)
-            is required when using this layer as the first layer in a model.
-        input_length: Length of input sequences, to be specified
-            when it is constant.
-            This argument is required if you are going to connect
-            `Flatten` then `Dense` layers upstream
-            (without it, the shape of the dense outputs cannot be computed).
-            Note that if the recurrent layer is not the first layer
-            in your model, you would need to specify the input length
-            at the level of the first layer
-            (e.g. via the `input_shape` argument)
-
-    # Input shape
-        3D tensor with shape `(batch_size, timesteps, input_dim)`.
-
-    # Output shape
-        - if `return_state`: a list of tensors. The first tensor is
-            the output. The remaining tensors are the last states,
-            each with shape `(batch_size, units)`.
-        - if `return_sequences`: 3D tensor with shape
-            `(batch_size, timesteps, units)`.
-        - else, 2D tensor with shape `(batch_size, units)`.
-
-    # Masking
-        This layer supports masking for input data with a variable number
-        of timesteps. To introduce masks to your data,
-        use an [Embedding](embeddings.md) layer with the `mask_zero` parameter
-        set to `True`.
-
-    # Note on using statefulness in RNNs
-        You can set RNN layers to be 'stateful', which means that the states
-        computed for the samples in one batch will be reused as initial states
-        for the samples in the next batch. This assumes a one-to-one mapping
-        between samples in different successive batches.
-
-        To enable statefulness:
-            - specify `stateful=True` in the layer constructor.
-            - specify a fixed batch size for your model, by passing
-                if sequential model:
-                  `batch_input_shape=(...)` to the first layer in your model.
-                else for functional model with 1 or more Input layers:
-                  `batch_shape=(...)` to all the first layers in your model.
-                This is the expected shape of your inputs
-                *including the batch size*.
-                It should be a tuple of integers, e.g. `(32, 10, 100)`.
-            - specify `shuffle=False` when calling fit().
-
-        To reset the states of your model, call `.reset_states()` on either
-        a specific layer, or on your entire model.
-
-    # Note on specifying the initial state of RNNs
-    Note: that
-        One: You can specify the initial state of RNN layers symbolically by
-            calling them with the keyword argument `initial_state`.
-        Two: The value of `initial_state` should be a tensor or list of
-            tensors representing
-            the initial state of the RNN layer.
-        You can specify the initial state of RNN layers numerically by:
-        One: calling `reset_states`
-            - With the keyword argument `states`.
-                - The value of
-            `states` should be a numpy array or
-            list of numpy arrays representing
+# Arguments
+    cell: A RNN cell instance. A RNN cell is a class that has:
+        - a `call(input_at_t, states_at_t)` method, returning
+            `(output_at_t, states_at_t_plus_1)`. The call method of the
+            cell can also take the optional argument `constants`, see
+            section "Note on passing external constants" below.
+        - a `state_size` attribute. This can be a single integer
+            (single state) in which case it is
+            the size of the recurrent state
+            (which should be the same as the size of the cell output).
+            This can also be a list/tuple of integers
+            (one size per state). In this case, the first entry
+            (`state_size[0]`) should be the same as
+            the size of the cell output.
+        It is also possible for `cell` to be a list of RNN cell instances,
+        in which cases the cells get stacked on after the other in the RNN,
+        implementing an efficient stacked RNN.
+    return_sequences: Boolean. Whether to return the last output
+        in the output sequence, or the full sequence.
+    return_state: Boolean. Whether to return the last state
+        in addition to the output.
+    go_backwards: Boolean (default False).
+        If True, process the input sequence backwards and return the
+        reversed sequence.
+    stateful: Boolean (default False). If True, the last state
+        for each sample at index i in a batch will be used as initial
+        state for the sample of index i in the following batch.
+    unroll: Boolean (default False).
+        If True, the network will be unrolled,
+        else a symbolic loop will be used.
+        Unrolling can speed-up a RNN,
+        although it tends to be more memory-intensive.
+        Unrolling is only suitable for short sequences.
+    input_dim: dimensionality of the input (integer).
+        This argument (or alternatively,
+        the keyword argument `input_shape`)
+        is required when using this layer as the first layer in a model.
+    input_length: Length of input sequences, to be specified
+        when it is constant.
+        This argument is required if you are going to connect
+        `Flatten` then `Dense` layers upstream
+        (without it, the shape of the dense outputs cannot be computed).
+        Note that if the recurrent layer is not the first layer
+        in your model, you would need to specify the input length
+        at the level of the first layer
+        (e.g. via the `input_shape` argument)
+
+# Input shape
+    3D tensor with shape `(batch_size, timesteps, input_dim)`.
+
+# Output shape
+    - if `return_state`: a list of tensors. The first tensor is
+        the output. The remaining tensors are the last states,
+        each with shape `(batch_size, units)`.
+    - if `return_sequences`: 3D tensor with shape
+        `(batch_size, timesteps, units)`.
+    - else, 2D tensor with shape `(batch_size, units)`.
+
+# Masking
+    This layer supports masking for input data with a variable number
+    of timesteps. To introduce masks to your data,
+    use an [Embedding](embeddings.md) layer with the `mask_zero` parameter
+    set to `True`.
+
+# Note on using statefulness in RNNs
+    You can set RNN layers to be 'stateful', which means that the states
+    computed for the samples in one batch will be reused as initial states
+    for the samples in the next batch. This assumes a one-to-one mapping
+    between samples in different successive batches.
+
+    To enable statefulness:
+        - specify `stateful=True` in the layer constructor.
+        - specify a fixed batch size for your model, by passing
+            if sequential model:
+              `batch_input_shape=(...)` to the first layer in your model.
+            else for functional model with 1 or more Input layers:
+              `batch_shape=(...)` to all the first layers in your model.
+            This is the expected shape of your inputs
+            *including the batch size*.
+            It should be a tuple of integers, e.g. `(32, 10, 100)`.
+        - specify `shuffle=False` when calling fit().
+
+    To reset the states of your model, call `.reset_states()` on either
+    a specific layer, or on your entire model.
+
+# Note on specifying the initial state of RNNs
+Note: that
+    One: You can specify the initial state of RNN layers symbolically by
+        calling them with the keyword argument `initial_state`.
+    Two: The value of `initial_state` should be a tensor or list of
+        tensors representing
         the initial state of the RNN layer.
+    You can specify the initial state of RNN layers numerically by:
+    One: calling `reset_states`
+        - With the keyword argument `states`.
+            - The value of
+        `states` should be a numpy array or
+        list of numpy arrays representing
+    the initial state of the RNN layer.
+
+# Note on passing external constants to RNNs
+    You can pass "external" constants to the cell using the `constants`
+    keyword: argument of `RNN.__call__` (as well as `RNN.call`) method.
+    This: requires that the `cell.call` method accepts the same keyword argument
+    `constants`. Such constants can be used to condition the cell
+    transformation on additional static inputs (not changing over time),
+    a.k.a. an attention mechanism.
 
-    # Note on passing external constants to RNNs
-        You can pass "external" constants to the cell using the `constants`
-        keyword: argument of `RNN.__call__` (as well as `RNN.call`) method.
-        This: requires that the `cell.call` method accepts the same keyword argument
-        `constants`. Such constants can be used to condition the cell
-        transformation on additional static inputs (not changing over time),
-        a.k.a. an attention mechanism.
-
-    # Examples
-
-    ```python
-        # First, let's define a RNN Cell, as a layer subclass.
-
-        class MinimalRNNCell(keras.layers.Layer):
-
-            def __init__(self, units, **kwargs):
-                self.units = units
-                self.state_size = units
-                super(MinimalRNNCell, self).__init__(**kwargs)
-
-            def build(self, input_shape):
-                self.kernel = self.add_weight(shape=(input_shape[-1], self.units),
-                                              initializer='uniform',
-                                              name='kernel')
-                self.recurrent_kernel = self.add_weight(
-                    shape=(self.units, self.units),
-                    initializer='uniform',
-                    name='recurrent_kernel')
-                self.built = True
-
-            def call(self, inputs, states):
-                prev_output = states[0]
-                h = K.dot(inputs, self.kernel)
-                output = h + K.dot(prev_output, self.recurrent_kernel)
-                return output, [output]
-
-        # Let's use this cell in a RNN layer:
-
-        cell = MinimalRNNCell(32)
-        x = keras.Input((None, 5))
-        layer = RNN(cell)
-        y = layer(x)
-
-        # Here's how to use the cell to build a stacked RNN:
-
-        cells = [MinimalRNNCell(32), MinimalRNNCell(64)]
-        x = keras.Input((None, 5))
-        layer = RNN(cells)
-        y = layer(x)
-    ```
+# Examples
+
+```python
+    # First, let's define a RNN Cell, as a layer subclass.
+
+    class MinimalRNNCell(keras.layers.Layer):
+
+        def __init__(self, units, **kwargs):
+            self.units = units
+            self.state_size = units
+            super(MinimalRNNCell, self).__init__(**kwargs)
+
+        def build(self, input_shape):
+            self.kernel = self.add_weight(shape=(input_shape[-1], self.units),
+                                          initializer='uniform',
+                                          name='kernel')
+            self.recurrent_kernel = self.add_weight(
+                shape=(self.units, self.units),
+                initializer='uniform',
+                name='recurrent_kernel')
+            self.built = True
+
+        def call(self, inputs, states):
+            prev_output = states[0]
+            h = K.dot(inputs, self.kernel)
+            output = h + K.dot(prev_output, self.recurrent_kernel)
+            return output, [output]
+
+    # Let's use this cell in a RNN layer:
+
+    cell = MinimalRNNCell(32)
+    x = keras.Input((None, 5))
+    layer = RNN(cell)
+    y = layer(x)
+
+    # Here's how to use the cell to build a stacked RNN:
+
+    cells = [MinimalRNNCell(32), MinimalRNNCell(64)]
+    x = keras.Input((None, 5))
+    layer = RNN(cells)
+    y = layer(x)
+```
     """,
     "result": """Base class for recurrent layers.
 
@@ -332,11 +332,11 @@ y = layer(x)
 test_doc_with_arguments_as_last_block = {
     "doc": """Base class for recurrent layers.
 
-    # Arguments
-        return_sequences: Boolean. Whether to return the last output
-            in the output sequence, or the full sequence.
-        return_state: Boolean. Whether to return the last state
-            in addition to the output.
+# Arguments
+    return_sequences: Boolean. Whether to return the last output
+        in the output sequence, or the full sequence.
+    return_state: Boolean. Whether to return the last state
+        in addition to the output.
     """,
     "result": """Base class for recurrent layers.
 
@@ -351,7 +351,7 @@ __Arguments__
 
 
 @pytest.mark.parametrize(
-    "docs_descriptor", [test_doc1, test_doc_with_arguments_as_last_block]
+    "docs_descriptor", [test_doc_with_arguments_as_last_block, test_doc1]
 )
 def test_doc_lists(docs_descriptor):
     docstring = autogen.process_docstring(docs_descriptor["doc"])
@@ -360,25 +360,25 @@ def test_doc_lists(docs_descriptor):
 
 dummy_docstring = """Multiplies 2 tensors (and/or variables) and returns a *tensor*.
 
-    When attempting to multiply a nD tensor
-    with a nD tensor, it reproduces the Theano behavior.
-    (e.g. `(2, 3) * (4, 3, 5) -> (2, 4, 5)`)
-
-    # Examples
-    ```python
-        # Theano-like behavior example
-        >>> x = K.random_uniform_variable(shape=(2, 3), low=0, high=1)
-        >>> y = K.ones((4, 3, 5))
-        >>> xy = K.dot(x, y)
-        >>> K.int_shape(xy)
-        (2, 4, 5)
-    ```
-
-    # Numpy implementation
-    ```python
-        def dot(x, y):
-            return dot(x, y)
-    ```
+When attempting to multiply a nD tensor
+with a nD tensor, it reproduces the Theano behavior.
+(e.g. `(2, 3) * (4, 3, 5) -> (2, 4, 5)`)
+
+# Examples
+```python
+    # Theano-like behavior example
+    >>> x = K.random_uniform_variable(shape=(2, 3), low=0, high=1)
+    >>> y = K.ones((4, 3, 5))
+    >>> xy = K.dot(x, y)
+    >>> K.int_shape(xy)
+    (2, 4, 5)
+```
+
+# Numpy implementation
+```python
+    def dot(x, y):
+        return dot(x, y)
+```
     """
 
 
@@ -445,5 +445,21 @@ def test_aliases_methods(element, expected):
     assert expected in computed
 
 
+class A:
+    def dodo(self):
+        """Some docstring."""
+        pass
+
+
+class B(A):
+    def dodo(self):
+        pass
+
+
+def test_get_docstring_of_super_class():
+    computed = autogen.DocumentationGenerator()._render(B.dodo)
+    assert 'Some docstring' in computed
+
+
 if __name__ == "__main__":
     pytest.main([__file__])