Repository: rr-/docstring_parser

Issue:
Examples attribute is empty when using numpydoc parser
https://github.com/rr-/docstring_parser/blob/ba7c874cd7bb483f9af4c9fdd2e0f3c68f446e0a/docstring_parser/common.py#L206

This often returns empty when using the numpydoc parser because the type of example is DocstringMeta, not child class DocstringExample.

Comments:

Patch:
diff --git a/docstring_parser/common.py b/docstring_parser/common.py
index 16f14d9..e3e5a56 100644
--- a/docstring_parser/common.py
+++ b/docstring_parser/common.py
@@ -137,10 +137,12 @@ class DocstringExample(DocstringMeta):
     def __init__(
         self,
         args: T.List[str],
+        snippet: T.Optional[str],
         description: T.Optional[str],
     ) -> None:
         """Initialize self."""
         super().__init__(args, description)
+        self.snippet = snippet
         self.description = description
 
 
diff --git a/docstring_parser/google.py b/docstring_parser/google.py
index a31e4a5..2eca8f4 100644
--- a/docstring_parser/google.py
+++ b/docstring_parser/google.py
@@ -136,7 +136,9 @@ class GoogleParser:
                 args=[section.key], description=desc, type_name=None
             )
         if section.key in EXAMPLES_KEYWORDS:
-            return DocstringExample(args=[section.key], description=desc)
+            return DocstringExample(
+                args=[section.key], snippet=None, description=desc
+            )
         if section.key in PARAM_KEYWORDS:
             raise ParseError("Expected paramenter name.")
         return DocstringMeta(args=[section.key], description=desc)
diff --git a/docstring_parser/numpydoc.py b/docstring_parser/numpydoc.py
index 7920f6c..e613f0f 100644
--- a/docstring_parser/numpydoc.py
+++ b/docstring_parser/numpydoc.py
@@ -7,10 +7,12 @@ import inspect
 import itertools
 import re
 import typing as T
+from textwrap import dedent
 
 from .common import (
     Docstring,
     DocstringDeprecated,
+    DocstringExample,
     DocstringMeta,
     DocstringParam,
     DocstringRaises,
@@ -223,6 +225,44 @@ class DeprecationSection(_SphinxSection):
         )
 
 
+class ExamplesSection(Section):
+    """Parser for numpydoc examples sections.
+
+    E.g. any section that looks like this:
+        >>> import numpy.matlib
+        >>> np.matlib.empty((2, 2))    # filled with random data
+        matrix([[  6.76425276e-320,   9.79033856e-307], # random
+                [  7.39337286e-309,   3.22135945e-309]])
+        >>> np.matlib.empty((2, 2), dtype=int)
+        matrix([[ 6600475,        0], # random
+                [ 6586976, 22740995]])
+    """
+
+    def parse(self, text: str) -> T.Iterable[DocstringMeta]:
+        """Parse ``DocstringExample`` objects from the body of this section.
+
+        :param text: section body text. Should be cleaned with
+                     ``inspect.cleandoc`` before parsing.
+        """
+        lines = dedent(text).strip().splitlines()
+        while lines:
+            snippet_lines = []
+            description_lines = []
+            while lines:
+                if not lines[0].startswith(">>>"):
+                    break
+                snippet_lines.append(lines.pop(0))
+            while lines:
+                if lines[0].startswith(">>>"):
+                    break
+                description_lines.append(lines.pop(0))
+            yield DocstringExample(
+                [self.key],
+                snippet="\n".join(snippet_lines) if snippet_lines else None,
+                description="\n".join(description_lines),
+            )
+
+
 DEFAULT_SECTIONS = [
     ParamSection("Parameters", "param"),
     ParamSection("Params", "param"),
@@ -244,8 +284,8 @@ DEFAULT_SECTIONS = [
     ReturnsSection("Return", "returns"),
     YieldsSection("Yields", "yields"),
     YieldsSection("Yield", "yields"),
-    Section("Examples", "examples"),
-    Section("Example", "examples"),
+    ExamplesSection("Examples", "examples"),
+    ExamplesSection("Example", "examples"),
     Section("Warnings", "warnings"),
     Section("Warning", "warnings"),
     Section("See Also", "see_also"),
diff --git a/pyproject.toml b/pyproject.toml
index 76edebe..5136a22 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -59,6 +59,7 @@ disable = [
     "import-error",
     "duplicate-code",
     "too-many-locals",
+    "too-many-lines",
     "too-many-branches",
     "too-many-statements",
     "too-many-arguments",


Test patch:
diff --git a/docstring_parser/tests/test_numpydoc.py b/docstring_parser/tests/test_numpydoc.py
index 57c6ac6..60ef1c9 100644
--- a/docstring_parser/tests/test_numpydoc.py
+++ b/docstring_parser/tests/test_numpydoc.py
@@ -660,20 +660,80 @@ def test_simple_sections() -> None:
     assert docstring.meta[3].args == ["references"]
 
 
-def test_examples() -> None:
+@pytest.mark.parametrize(
+    "source, expected_results",
+    [
+        (
+            "Description\nExamples\n--------\nlong example\n\nmore here",
+            [
+                (None, "long example\n\nmore here"),
+            ],
+        ),
+        (
+            "Description\nExamples\n--------\n>>> test",
+            [
+                (">>> test", ""),
+            ],
+        ),
+        (
+            "Description\nExamples\n--------\n>>> testa\n>>> testb",
+            [
+                (">>> testa\n>>> testb", ""),
+            ],
+        ),
+        (
+            "Description\nExamples\n--------\n>>> test1\ndesc1",
+            [
+                (">>> test1", "desc1"),
+            ],
+        ),
+        (
+            "Description\nExamples\n--------\n"
+            ">>> test1a\n>>> test1b\ndesc1a\ndesc1b",
+            [
+                (">>> test1a\n>>> test1b", "desc1a\ndesc1b"),
+            ],
+        ),
+        (
+            "Description\nExamples\n--------\n"
+            ">>> test1\ndesc1\n>>> test2\ndesc2",
+            [
+                (">>> test1", "desc1"),
+                (">>> test2", "desc2"),
+            ],
+        ),
+        (
+            "Description\nExamples\n--------\n"
+            ">>> test1a\n>>> test1b\ndesc1a\ndesc1b\n"
+            ">>> test2a\n>>> test2b\ndesc2a\ndesc2b\n",
+            [
+                (">>> test1a\n>>> test1b", "desc1a\ndesc1b"),
+                (">>> test2a\n>>> test2b", "desc2a\ndesc2b"),
+            ],
+        ),
+        (
+            "Description\nExamples\n--------\n"
+            "    >>> test1a\n    >>> test1b\n    desc1a\n    desc1b\n"
+            "    >>> test2a\n    >>> test2b\n    desc2a\n    desc2b\n",
+            [
+                (">>> test1a\n>>> test1b", "desc1a\ndesc1b"),
+                (">>> test2a\n>>> test2b", "desc2a\ndesc2b"),
+            ],
+        ),
+    ],
+)
+def test_examples(
+    source, expected_results: T.List[T.Tuple[T.Optional[str], str]]
+) -> None:
     """Test parsing examples."""
-    docstring = parse(
-        """
-        Short description
-        Examples
-        --------
-        long example
-
-        more here
-        """
-    )
-    assert len(docstring.meta) == 1
-    assert docstring.meta[0].description == "long example\n\nmore here"
+    docstring = parse(source)
+    assert len(docstring.meta) == len(expected_results)
+    for meta, expected_result in zip(docstring.meta, expected_results):
+        assert meta.description == expected_result[1]
+    assert len(docstring.examples) == len(expected_results)
+    for example, expected_result in zip(docstring.examples, expected_results):
+        assert example.snippet == expected_result[0]
+        assert example.description == expected_result[1]
 
 
 @pytest.mark.parametrize(
@@ -944,6 +1004,32 @@ def test_deprecation(
             "    ValueError\n"
             "        description",
         ),
+        (
+            """
+            Description
+            Examples:
+            --------
+            >>> test1a
+            >>> test1b
+            desc1a
+            desc1b
+            >>> test2a
+            >>> test2b
+            desc2a
+            desc2b
+            """,
+            "Description\n"
+            "Examples:\n"
+            "--------\n"
+            ">>> test1a\n"
+            ">>> test1b\n"
+            "desc1a\n"
+            "desc1b\n"
+            ">>> test2a\n"
+            ">>> test2b\n"
+            "desc2a\n"
+            "desc2b",
+        ),
     ],
 )
 def test_compose(source: str, expected: str) -> None: