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

ENH: Add support for reST-style docstrings #274

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
100 changes: 92 additions & 8 deletions pdoc/html_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,71 @@ def googledoc_sections(match):
r'((?:\n?(?: {2,}.*|$))+)', re.MULTILINE).sub(googledoc_sections, text)
return text

@staticmethod
def reST(text: str) -> str:
"""
Convert `text` in reST-style docstring format to Markdown
to be further converted later.
"""
def reST_sections(match) -> str:
# E.g. for ":param arg1: Text" tag is "param", name is "arg1", and body is "Text"
tag, name, body = match.groups('')
body = textwrap.dedent(body)

type_ = None
nonlocal active_section
active_section_changed = False

if tag in ['type', 'rtype']:
return ''
elif tag in ('param', 'parameter', 'arg', 'argument', 'key', 'keyword'):
type_ = parameter_types.get(name, None)

if active_section != 'Args':
active_section = 'Args'
active_section_changed = True
elif tag in ('return', 'returns'):
if len(return_types) > 0:
type_ = return_types.pop()

if active_section != 'Returns':
active_section = 'Returns'
active_section_changed = True
elif tag in ('raise', 'raises'):
if active_section != 'Raises':
active_section = 'Raises'
active_section_changed = True

if name or type_:
text = _ToMarkdown._deflist(*_ToMarkdown._fix_indent(name, type_, body))
else:
_, _, body = _ToMarkdown._fix_indent(name, type_, body)
text = f': {body}'

if active_section_changed:
text = f'\n{active_section}:\n-----=\n{text}'
else:
text = f'\n{text}'

return text

regex = re.compile(r'^:(\S+)(?:\s(\S+?))?:((?:\n?(?: .*|$))+)', re.MULTILINE)

# Get all parameter and return types beforehand, to then use them when substituting
# the sections
parameter_types = {}
return_types = []
for tag, name, body in regex.findall(text):
if tag == 'type':
parameter_types[name] = body.strip()
elif tag == 'rtype':
return_types.append(body.strip())

active_section = None # Keep track of the currently active section (e.g. Args, Returns)
text = regex.sub(reST_sections, text)

return text

@staticmethod
def _admonition(match, module=None, limit_types=None):
indent, type, value, text = match.groups()
Expand Down Expand Up @@ -406,8 +471,9 @@ def to_html(text: str, *,
latex_math: bool = False):
"""
Returns HTML of `text` interpreted as `docformat`. `__docformat__` is respected
if present, otherwise Numpydoc and Google-style docstrings are assumed,
as well as pure Markdown.
if present, otherwise it is inferred whether it's reST-style, or Numpydoc
and Google-style docstrings. Pure Markdown and reST directives are also assumed
and processed if docformat has not been specified.

`module` should be the documented module (so the references can be
resolved) and `link` is the hyperlinking function like the one in the
Expand All @@ -430,20 +496,35 @@ def to_markdown(text: str, *,
module: pdoc.Module = None, link: Callable[..., str] = None):
"""
Returns `text`, assumed to be a docstring in `docformat`, converted to markdown.
`__docformat__` is respected
if present, otherwise Numpydoc and Google-style docstrings are assumed,
as well as pure Markdown.
`__docformat__` is respected if present, otherwise it is inferred whether it's
reST-style, or Numpydoc and Google-style docstrings. Pure Markdown and reST directives
are also assumed and processed if docformat has not been specified.

`module` should be the documented module (so the references can be
resolved) and `link` is the hyperlinking function like the one in the
example template.
"""
if not docformat:
docformat = str(getattr(getattr(module, 'obj', None), '__docformat__', 'numpy,google '))
docformat = str(getattr(getattr(module, 'obj', None), '__docformat__', ''))

# Infer docformat if it hasn't been specified
if docformat == '':
reST_tags = ['param', 'arg', 'type', 'raise', 'except', 'return', 'rtype']
reST_regex = fr'^:(?:{"|".join(reST_tags)}).*?:'
found_reST_tags = re.findall(reST_regex, text, re.MULTILINE)

# Assume reST-style docstring if any of the above specified tags is present at the beginning of a line.
# Could make this more robust, e.g., by checking against the amount of found google or numpy tags
if len(found_reST_tags) > 0:
docformat = 'reST '
else:
docformat = 'numpy,google '

docformat, *_ = docformat.lower().split()
if not (set(docformat.split(',')) & {'', 'numpy', 'google'}):

if not (set(docformat.split(',')) & {'', 'numpy', 'google', 'rest'}):
warn('__docformat__ value {!r} in module {!r} not supported. '
'Supported values are: numpy, google.'.format(docformat, module))
'Supported values are: numpy, google, reST.'.format(docformat, module))
docformat = 'numpy,google'

with _fenced_code_blocks_hidden(text) as result:
Expand All @@ -462,6 +543,9 @@ def to_markdown(text: str, *,
if 'numpy' in docformat:
text = _ToMarkdown.numpy(text)

if 'rest' in docformat:
text = _ToMarkdown.reST(text)

if module and link:
# Hyperlink markdown code spans not within markdown hyperlinks.
# E.g. `code` yes, but not [`code`](...). RE adapted from:
Expand Down
30 changes: 30 additions & 0 deletions pdoc/test/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1403,6 +1403,36 @@ def test_google(self):
html = to_html(text, module=self._module, link=self._link)
self.assertEqual(html, expected)

def test_reST(self):
expected = '''<p>Summary line.</p>
<h2 id="args">Args:</h2>
<dl>
<dt><strong><code>arg1</code></strong> :&ensp;<code>int</code></dt>
<dd>Text1</dd>
<dt><strong><code>arg2</code></strong> :&ensp;<code>Optional[List[Tuple[str]]]</code></dt>
<dd>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed
diam nonumy eirmod tempor invidunt</dd>
<dt><strong><code>arg_arg_3</code></strong> :&ensp;<code>Dict[int, Dict[str, Any]]</code></dt>
<dd>Y:=H^T<em>X!@#$%^&amp;&amp;</em>()_[]{}';'::</dd>
</dl>
<h2 id="returns">Returns:</h2>
<dl>
<dt><code>bool</code></dt>
<dd>True. Or False. Depends</dd>
<dd>Now with more "s"</dd>
</dl>
<h2 id="raises">Raises:</h2>
<dl>
<dt><strong><code>Exception</code></strong></dt>
<dd>Raised occasionally</dd>
<dt><strong><code>ZeroDivisionError</code></strong></dt>
<dd>You know why and when</dd>
</dl>'''
text = inspect.getdoc(self._docmodule.reST)
html = to_html(text, module=self._module, link=self._link)

self.assertEqual(html, expected)

def test_doctests(self):
expected = '''<p>Need an intro paragrapgh.</p>
<pre><code>&gt;&gt;&gt; Then code is indented one level
Expand Down
22 changes: 22 additions & 0 deletions pdoc/test/example_pkg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,25 @@ def doctests(self):
Exception: something went wrong
"""

def reST(self):
"""
Summary line.

:param arg1: Text1
:parameter arg2: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed
diam nonumy eirmod tempor invidunt
:arg arg_arg_3: Y:=H^T*X!@#$%^&&*()_[]{}';'::
:type arg1: int
:type arg2: Optional[List[Tuple[str]]]
:type arg_arg_3: Dict[int, Dict[str, Any]]
:return: True. Or False. Depends
:rtype: bool

:returns: Now with more "s"
:raise Exception: Raised occasionally
:raises ZeroDivisionError: You know why and when
"""

def reST_directives(self):
"""
.. todo::
Expand Down Expand Up @@ -345,6 +364,9 @@ def reST_directives(self):
doctests = Docformats.doctests


reST = Docformats.reST


reST_directives = Docformats.reST_directives


Expand Down