diff --git a/docs/developers/test_suite/test_parsers/test_docstrings.md b/docs/developers/test_suite/test_parsers/test_docstrings.md deleted file mode 100644 index d0612a3..0000000 --- a/docs/developers/test_suite/test_parsers/test_docstrings.md +++ /dev/null @@ -1 +0,0 @@ -::: tests.test_parsers.test_docstrings diff --git a/docs/developers/test_suite/test_parsers/test_docstrings/test_google.md b/docs/developers/test_suite/test_parsers/test_docstrings/test_google.md new file mode 100644 index 0000000..031cd7f --- /dev/null +++ b/docs/developers/test_suite/test_parsers/test_docstrings/test_google.md @@ -0,0 +1 @@ +::: tests.test_parsers.test_docstrings.test_google diff --git a/docs/reference/parsers/docstrings.md b/docs/reference/parsers/docstrings.md deleted file mode 100644 index 841e24d..0000000 --- a/docs/reference/parsers/docstrings.md +++ /dev/null @@ -1 +0,0 @@ -::: pytkdocs.parsers.docstrings diff --git a/docs/reference/parsers/docstrings/__init__.md b/docs/reference/parsers/docstrings/__init__.md new file mode 100644 index 0000000..234c314 --- /dev/null +++ b/docs/reference/parsers/docstrings/__init__.md @@ -0,0 +1 @@ +::: pytkdocs.parsers.docstrings.__init__ diff --git a/docs/reference/parsers/docstrings/base.md b/docs/reference/parsers/docstrings/base.md new file mode 100644 index 0000000..00c65a4 --- /dev/null +++ b/docs/reference/parsers/docstrings/base.md @@ -0,0 +1 @@ +::: pytkdocs.parsers.docstrings.base diff --git a/docs/reference/parsers/docstrings/google.md b/docs/reference/parsers/docstrings/google.md new file mode 100644 index 0000000..553d7c5 --- /dev/null +++ b/docs/reference/parsers/docstrings/google.md @@ -0,0 +1 @@ +::: pytkdocs.parsers.docstrings.google diff --git a/mkdocs.yml b/mkdocs.yml index af49daf..d01da6c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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" @@ -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" diff --git a/src/pytkdocs/parsers/docstrings/base.py b/src/pytkdocs/parsers/docstrings/base.py index 272877d..d5b93b3 100644 --- a/src/pytkdocs/parsers/docstrings/base.py +++ b/src/pytkdocs/parsers/docstrings/base.py @@ -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. diff --git a/src/pytkdocs/parsers/docstrings/google.py b/src/pytkdocs/parsers/docstrings/google.py index 7f9bb13..53ba33a 100644 --- a/src/pytkdocs/parsers/docstrings/google.py +++ b/src/pytkdocs/parsers/docstrings/google.py @@ -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\s*)(?P[\w-]+):((?:\s+)(?P.+))?$") """Regular expressions to match lines starting admonitions, of the form `TYPE: [TITLE]`.""" @@ -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]) @@ -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 diff --git a/src/pytkdocs/serializer.py b/src/pytkdocs/serializer.py index 00a77e7..bf718ca 100644 --- a/src/pytkdocs/serializer.py +++ b/src/pytkdocs/serializer.py @@ -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 diff --git a/tests/test_parsers/test_docstrings/test_google.py b/tests/test_parsers/test_docstrings/test_google.py index 59d1479..3986aba 100644 --- a/tests/test_parsers/test_docstrings/test_google.py +++ b/tests/test_parsers/test_docstrings/test_google.py @@ -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 @@ -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."""