diff --git a/CHANGELOG.md b/CHANGELOG.md index b7e2a211..22519608 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ # Unreleased: pdoc next + - Add support for reStructuredText field lists: `:param foo: text`. + ([#275](https://github.com/mitmproxy/pdoc/issues/275), [@mhils](https://github.com/mhils)) # 2022-03-23: pdoc 10.0.4 diff --git a/pdoc/doc.py b/pdoc/doc.py index 0bf1156a..610a549f 100644 --- a/pdoc/doc.py +++ b/pdoc/doc.py @@ -230,8 +230,8 @@ def own_members(self) -> list[Doc]: @cached_property def members(self) -> dict[str, Doc]: - """A mapping from all members to their documentation objects. - + """A mapping from all members to their documentation objects. + This mapping includes private members; they are only filtered out as part of the template logic. """ diff --git a/pdoc/docstrings.py b/pdoc/docstrings.py index 377266cc..f5266974 100644 --- a/pdoc/docstrings.py +++ b/pdoc/docstrings.py @@ -150,11 +150,11 @@ def numpy(docstring: str) -> str: "Warns", "Attributes", ): - contents += f"###### {heading}\n" f"{_numpy_parameters(content)}" + contents += f"###### {heading}\n{_numpy_parameters(content)}" elif heading == "See Also": - contents += f"###### {heading}\n" f"{_numpy_seealso(content)}" + contents += f"###### {heading}\n{_numpy_seealso(content)}" else: - contents += f"###### {heading}\n" f"{dedent(content)}" + contents += f"###### {heading}\n{dedent(content)}" contents += tail return contents @@ -219,6 +219,8 @@ def rst(contents: str, source_file: Path | None) -> str: contents = _rst_footnotes(contents) + contents = _rst_fields(contents) + return contents @@ -362,3 +364,60 @@ def _rst_admonition(m: re.Match[str]) -> str: contents, flags=re.MULTILINE | re.VERBOSE, ) + + +def _rst_fields(contents: str) -> str: + """ + Convert reStructuredText fields to Markdown. + + """ + + _has_parameter_section = False + _has_raises_section = False + + def _rst_field(m: re.Match[str]) -> str: + type = m["type"] + body = m["body"] + + if m["name"]: + name = f"**{m['name'].strip()}**: " + else: + name = "" + + if type == "param": + nonlocal _has_parameter_section + text = f" - {name}{body}" + if not _has_parameter_section: + _has_parameter_section = True + text = "\n###### Parameters\n" + text + return text + elif type == "type": + return "" # we expect users to use modern type annotations. + elif type == "return": + body = indent(body, "> ", lambda line: True) + return f"\n###### Returns\n{body}" + elif type == "rtype": + return "" # we expect users to use modern type annotations. + elif type == "raises": + nonlocal _has_raises_section + text = f" - {name}{body}" + if not _has_raises_section: + _has_raises_section = True + text = "\n###### Raises\n" + text + return text + else: # pragma: no cover + raise AssertionError("unreachable") + + field = "param|type|return|rtype|raises" + return re.sub( + rf""" + ^:(?P{field})(?:[ ]+(?P.+))?: + (?P.*( + (?:\n[ ]*)* # maybe some empty lines followed by + [ ]+.+ # lines with indentation + )*(?:\n|$)) + """, + _rst_field, + contents, + flags=re.MULTILINE | re.VERBOSE, + ) diff --git a/test/testdata/flavors_rst.html b/test/testdata/flavors_rst.html index cc577059..6fcb8549 100644 --- a/test/testdata/flavors_rst.html +++ b/test/testdata/flavors_rst.html @@ -56,6 +56,18 @@

API Documentation

  • include
  • +
  • + fields +
  • +
  • + fields_text_after_param +
  • +
  • + fields_invalid +
  • +
  • + fields_exception +
  • @@ -190,7 +202,7 @@

    def footnote4(): """ - There is not footnote for this reference [#]_. + There is no footnote for this reference [#]_. """ @@ -200,6 +212,45 @@

    .. include:: flavors_rst_include/include.rst """ + + +def fields(foo: str = None, bar: bool = True) -> str: + """This method has field descriptions. + + :param foo: A string, + defaults to None + :type foo: string, optional + :param bar: Another + boolean. + :return: Another string, + or maybe `None`. + :rtype: A string. + """ + + +def fields_text_after_param(foo): + """This method has text after the `:param` fields. + + :param foo: Some text. + + Here's some more text. + """ + + +def fields_invalid(foo: str = None) -> str: + """This method has invalid `:param` definitions. + + :param: What is this for? + + :unknown: This is an unknown field name. + """ + + +def fields_exception(): + """ + :raises RuntimeError: Some multi-line + exception description. + """ @@ -574,13 +625,13 @@

    View Source
    def footnote4():
         """
    -    There is not footnote for this reference [#]_.
    +    There is no footnote for this reference [#]_.
         """
     
    -

    There is not footnote for this reference [#]_.

    +

    There is no footnote for this reference [#]_.

    @@ -615,6 +666,150 @@

    + +
    +
    #   + + + def + fields(foo: str = None, bar: bool = True) -> str: +
    + +
    + View Source +
    def fields(foo: str = None, bar: bool = True) -> str:
    +    """This method has field descriptions.
    +
    +    :param foo: A string,
    +        defaults to None
    +    :type foo: string, optional
    +    :param bar: Another
    +     boolean.
    +    :return: Another string,
    +        or maybe `None`.
    +    :rtype: A string.
    +    """
    +
    + +
    + +

    This method has field descriptions.

    + +
    Parameters
    + +
      +
    • foo: A string, +defaults to None
    • +
    • bar: Another +boolean.
    • +
    + +
    Returns
    + +
    +

    Another string, + or maybe None.

    +
    +
    + + +
    +
    +
    #   + + + def + fields_text_after_param(foo): +
    + +
    + View Source +
    def fields_text_after_param(foo):
    +    """This method has text after the `:param` fields.
    +
    +    :param foo: Some text.
    +
    +    Here's some more text.
    +    """
    +
    + +
    + +

    This method has text after the :param fields.

    + +
    Parameters
    + +
      +
    • foo: Some text.
    • +
    + +

    Here's some more text.

    +
    + + +
    +
    +
    #   + + + def + fields_invalid(foo: str = None) -> str: +
    + +
    + View Source +
    def fields_invalid(foo: str = None) -> str:
    +    """This method has invalid `:param` definitions.
    +
    +    :param: What is this for?
    +
    +    :unknown: This is an unknown field name.
    +    """
    +
    + +
    + +

    This method has invalid :param definitions.

    + +
    Parameters
    + +
      +
    • What is this for?
    • +
    + +

    :unknown: This is an unknown field name.

    +
    + + +
    +
    +
    #   + + + def + fields_exception(): +
    + +
    + View Source +
    def fields_exception():
    +    """
    +    :raises RuntimeError: Some multi-line
    +        exception description.
    +    """
    +
    + +
    + +
    Raises
    + +
      +
    • RuntimeError: Some multi-line +exception description.
    • +
    +
    + +
    diff --git a/test/testdata/flavors_rst.py b/test/testdata/flavors_rst.py index 14821104..f25ac759 100644 --- a/test/testdata/flavors_rst.py +++ b/test/testdata/flavors_rst.py @@ -113,7 +113,7 @@ def footnote3(): def footnote4(): """ - There is not footnote for this reference [#]_. + There is no footnote for this reference [#]_. """ @@ -123,3 +123,42 @@ def include(): .. include:: flavors_rst_include/include.rst """ + + +def fields(foo: str = None, bar: bool = True) -> str: + """This method has field descriptions. + + :param foo: A string, + defaults to None + :type foo: string, optional + :param bar: Another + boolean. + :return: Another string, + or maybe `None`. + :rtype: A string. + """ + + +def fields_text_after_param(foo): + """This method has text after the `:param` fields. + + :param foo: Some text. + + Here's some more text. + """ + + +def fields_invalid(foo: str = None) -> str: + """This method has invalid `:param` definitions. + + :param: What is this for? + + :unknown: This is an unknown field name. + """ + + +def fields_exception(): + """ + :raises RuntimeError: Some multi-line + exception description. + """ diff --git a/test/testdata/flavors_rst.txt b/test/testdata/flavors_rst.txt index d8f7677b..3c246308 100644 --- a/test/testdata/flavors_rst.txt +++ b/test/testdata/flavors_rst.txt @@ -12,5 +12,9 @@ - - > \ No newline at end of file + + + str: ... # This method has fiel…> + + str: ... # This method has inva…> + > \ No newline at end of file