Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support examples section #35

Merged
merged 2 commits into from
Jun 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion docs/developers/test_suite/test_parsers/test_docstrings.md

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: tests.test_parsers.test_docstrings.test_google
1 change: 0 additions & 1 deletion docs/reference/parsers/docstrings.md

This file was deleted.

1 change: 1 addition & 0 deletions docs/reference/parsers/docstrings/__init__.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: pytkdocs.parsers.docstrings.__init__
1 change: 1 addition & 0 deletions docs/reference/parsers/docstrings/base.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: pytkdocs.parsers.docstrings.base
1 change: 1 addition & 0 deletions docs/reference/parsers/docstrings/google.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: pytkdocs.parsers.docstrings.google
8 changes: 6 additions & 2 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ nav:
- parsers:
- __init__.py: "reference/parsers/__init__.md"
- attributes.py: "reference/parsers/attributes.md"
- docstrings.py: "reference/parsers/docstrings.md"
- docstrings:
- __init__.py: "reference/parsers/docstrings/__init__.md"
- base.py: "reference/parsers/docstrings/base.md"
- google.py: "reference/parsers/docstrings/google.md"
- properties.py: "reference/properties.md"
- serializer.py: "reference/serializer.md"
- Contributing: "contributing.md"
Expand All @@ -37,7 +40,8 @@ nav:
- test_objects.py: "developers/test_suite/test_objects.md"
- test_parsers:
- test_attributes.py: "developers/test_suite/test_parsers/test_attributes.md"
- test_docstrings.py: "developers/test_suite/test_parsers/test_docstrings.md"
- test_docstrings:
- test_google.py: "developers/test_suite/test_parsers/test_docstrings/test_google.md"
- test_properties.py: "developers/test_suite/test_properties.md"
- test_serializer.py: "developers/test_suite/test_serializer.md"
- Code of Conduct: "code_of_conduct.md"
Expand Down
5 changes: 2 additions & 3 deletions src/pytkdocs/parsers/docstrings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,9 @@ class Type:
PARAMETERS = "parameters"
EXCEPTIONS = "exceptions"
RETURN = "return"
EXAMPLES = "examples"

def __init__(
self, section_type: str, value: Union[str, List[Parameter], List[AnnotatedObject], AnnotatedObject]
) -> None:
def __init__(self, section_type: str, value: Any) -> None:
"""
Initialization method.

Expand Down
63 changes: 63 additions & 0 deletions src/pytkdocs/parsers/docstrings/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
TITLES_RETURN: Sequence[str] = ("return:", "returns:")
"""Titles to match for "returns" sections."""

TITLES_EXAMPLES: Sequence[str] = ("example:", "examples:")
"""Titles to match for "examples" sections."""

RE_GOOGLE_STYLE_ADMONITION: Pattern = re.compile(r"^(?P<indent>\s*)(?P<type>[\w-]+):((?:\s+)(?P<title>.+))?$")
"""Regular expressions to match lines starting admonitions, of the form `TYPE: [TITLE]`."""
Expand Down Expand Up @@ -75,6 +77,15 @@ def parse_sections(self, docstring: str) -> List[Section]: # noqa: D102
if section:
sections.append(section)

elif line_lower in TITLES_EXAMPLES:
if current_section:
if any(current_section):
sections.append(Section(Section.Type.MARKDOWN, "\n".join(current_section)))
current_section = []
section, i = self.read_examples_section(lines, i + 1)
if section:
sections.append(section)

elif line_lower.lstrip(" ").startswith("```"):
in_code_block = True
current_section.append(lines[i])
Expand Down Expand Up @@ -343,3 +354,55 @@ def read_return_section(self, lines: List[str], start_index: int) -> Tuple[Optio
return None, i

return Section(Section.Type.RETURN, AnnotatedObject(annotation, text)), i

def read_examples_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
"""
Parse an "examples" section.

Arguments:
lines: The examples block lines.
start_index: The line number to start at.

Returns:
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""

text, i = self.read_block(lines, start_index)

sub_sections = []
in_code_example = False
in_code_block = False
current_text = []
current_example = []

for line in text.split("\n"):
if self.is_empty_line(line):
if in_code_example:
if current_example:
sub_sections.append((Section.Type.EXAMPLES, "\n".join(current_example)))
current_example = []
in_code_example = False
else:
current_text.append(line)
elif in_code_example:
current_example.append(line)
elif line.startswith("```"):
in_code_block = not in_code_block
current_text.append(line)
elif in_code_block:
current_text.append(line)
elif line.startswith(">>>"):
if current_text:
sub_sections.append((Section.Type.MARKDOWN, "\n".join(current_text)))
current_text = []
in_code_example = True
current_example.append(line)
else:
current_text.append(line)

if current_text:
sub_sections.append((Section.Type.MARKDOWN, "\n".join(current_text)))
elif current_example:
sub_sections.append((Section.Type.EXAMPLES, "\n".join(current_example)))

return Section(Section.Type.EXAMPLES, sub_sections), i
2 changes: 2 additions & 0 deletions src/pytkdocs/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ def serialize_docstring_section(section: Section) -> dict:
serialized.update({"value": [serialize_annotated_object(e) for e in section.value]}) # type: ignore
elif section.type == section.Type.PARAMETERS:
serialized.update({"value": [serialize_parameter(p) for p in section.value]}) # type: ignore
elif section.type == section.Type.EXAMPLES:
serialized.update({"value": section.value}) # type: ignore
return serialized


Expand Down
68 changes: 67 additions & 1 deletion tests/test_parsers/test_docstrings/test_google.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Tests for [the `parsers.docstrings` module][pytkdocs.parsers.docstrings]."""
"""Tests for [the `parsers.docstrings.google` module][pytkdocs.parsers.docstrings.google]."""

import inspect
from textwrap import dedent
Expand Down Expand Up @@ -111,6 +111,72 @@ def f(x: int, y: int) -> int:
assert not errors


def test_function_with_examples():
"""Parse a function docstring with signature annotations."""

def f(x: int, y: int) -> int:
"""
This function has annotations.

Examples:
Some examples that will create an unified code block:

>>> 2 + 2 == 5
False
>>> print("examples")
"examples"

This is just a random comment in the examples section.

These examples will generate two different code blocks. Note the blank line.

>>> print("I'm in the first code block!")
"I'm in the first code block!"

>>> print("I'm in other code block!")
"I'm in other code block!"

We also can write multiline examples:

>>> x = 3 + 2
>>> y = x + 10
>>> y
15

This is just a typical Python code block:

```python
print("examples")
return 2 + 2
```

Even if it contains doctests, the following block is still considered a normal code-block.

```python
>>> print("examples")
"examples"
>>> 2 + 2
4
```

The blank line before an example is optional.
>>> x = 3
>>> y = "apple"
>>> z = False
>>> l = [x, y, z]
>>> my_print_list_function(l)
3
"apple"
False
"""
return x + y

sections, errors = parse(inspect.getdoc(f), inspect.signature(f))
assert len(sections) == 2
assert len(sections[1].value) == 9
assert not errors


def test_types_in_docstring():
"""Parse types in docstring."""

Expand Down