Skip to content

Commit 572bdf2

Browse files
committed
REF: Refactor doctests and googledoc docformats
Fixes #163 Fixes #174
1 parent f3453db commit 572bdf2

File tree

4 files changed

+103
-35
lines changed

4 files changed

+103
-35
lines changed

pdoc/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,7 @@ def __init__(self, module: Union[ModuleType, str], *, docfilter: Callable[[Doc],
556556
"exported in `__all__`".format(self.module, name))
557557
else:
558558
def is_from_this_module(obj):
559-
mod = inspect.getmodule(obj)
559+
mod = inspect.getmodule(inspect.unwrap(obj))
560560
return mod is None or mod.__name__ == self.obj.__name__
561561

562562
public_objs = [(name, inspect.unwrap(obj))

pdoc/html_helpers.py

Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import os
66
import re
77
import subprocess
8+
import textwrap
89
import traceback
10+
from contextlib import contextmanager
911
from functools import partial, lru_cache
1012
from typing import Callable, Match
1113
from warnings import warn
@@ -91,6 +93,32 @@ def glimpse(text: str, max_length=153, *, paragraph=True,
9193
)
9294

9395

96+
@contextmanager
97+
def _fenced_code_blocks_hidden(text):
98+
def hide(text):
99+
def replace(match):
100+
orig = match.group()
101+
new = '@' + str(hash(orig)) + '@'
102+
hidden[new] = orig
103+
return new
104+
105+
text = re.compile(r'^(?P<fence>```|~~~).*\n'
106+
r'(?:.*\n)*?'
107+
r'^(?P=fence)(?!.)', re.MULTILINE).sub(replace, text)
108+
return text
109+
110+
def unhide(text):
111+
for k, v in hidden.items():
112+
text = text.replace(k, v)
113+
return text
114+
115+
hidden = {}
116+
# Via a manager object (a list) so modifications can pass back and forth as result[0]
117+
result = [hide(text)]
118+
yield result
119+
result[0] = unhide(result[0])
120+
121+
94122
class _ToMarkdown:
95123
"""
96124
This class serves as a namespace for methods converting common
@@ -157,17 +185,19 @@ def _numpy_sections(match):
157185
lists.
158186
"""
159187
section, body = match.groups()
160-
if section.title() == 'See Also':
188+
section = section.title()
189+
if section == 'See Also':
161190
body = re.sub(r'\n\s{4}\s*', ' ', body) # Handle line continuation
162191
body = re.sub(r'^((?:\n?[\w.]* ?: .*)+)|(.*\w.*)',
163192
_ToMarkdown._numpy_seealso, body)
164-
elif section.title() in ('Returns', 'Yields', 'Raises', 'Warns'):
193+
elif section in ('Returns', 'Yields', 'Raises', 'Warns'):
165194
body = re.sub(r'^(?:(?P<name>\*{0,2}\w+(?:, \*{0,2}\w+)*)'
166195
r'(?: ?: (?P<type>.*))|'
167196
r'(?P<just_type>\w[^\n`*]*))(?<!\.)$'
168197
r'(?P<desc>(?:\n(?: {4}.*|$))*)',
169198
_ToMarkdown._numpy_params, body, flags=re.MULTILINE)
170-
else:
199+
elif section in ('Parameters', 'Receives', 'Other Parameters',
200+
'Arguments', 'Args', 'Attributes'):
171201
name = r'(?:\w|\{\w+(?:,\w+)+\})+' # Support curly brace expansion
172202
body = re.sub(r'^(?P<name>\*{0,2}' + name + r'(?:, \*{0,2}' + name + r')*)'
173203
r'(?: ?: (?P<type>.*))?(?<!\.)$'
@@ -203,20 +233,29 @@ def indent(indent, text, *, clean_first=False):
203233
return re.sub(r'\n', '\n' + indent, indent + text.rstrip())
204234

205235
@staticmethod
206-
def google(text,
207-
_googledoc_sections=partial(
208-
re.compile(r'^([A-Z]\w+):$\n((?:\n?(?: {2,}.*|$))+)', re.MULTILINE).sub,
209-
lambda m, _params=partial(
210-
re.compile(r'^([\w*]+)(?: \(([\w.,=\[\] ]+)\))?: '
211-
r'((?:.*)(?:\n(?: {2,}.*|$))*)', re.MULTILINE).sub,
212-
lambda m: _ToMarkdown._deflist(*_ToMarkdown._fix_indent(*m.groups()))): (
213-
m.group() if not m.group(2) else '\n{}\n-----\n{}'.format(
214-
m.group(1), _params(inspect.cleandoc('\n' + m.group(2))))))):
236+
def google(text):
215237
"""
216238
Convert `text` in Google-style docstring format to Markdown
217239
to be further converted later.
218240
"""
219-
return _googledoc_sections(text)
241+
def googledoc_sections(match):
242+
section, body = match.groups('')
243+
if not body:
244+
return match.group()
245+
body = textwrap.dedent(body)
246+
section = section.title()
247+
if section in ('Args', 'Attributes', 'Returns', 'Yields', 'Raises', 'Warns'):
248+
body = re.compile(
249+
r'^([\w*]+)(?: \(([\w.,=\[\] ]+)\))?: '
250+
r'((?:.*)(?:\n(?: {2,}.*|$))*)', re.MULTILINE).sub(
251+
lambda m: _ToMarkdown._deflist(*_ToMarkdown._fix_indent(*m.groups())),
252+
inspect.cleandoc('\n' + body)
253+
)
254+
return '\n{}\n-----\n{}'.format(section, body)
255+
256+
text = re.compile(r'^([A-Z]\w+):$\n'
257+
r'((?:\n?(?: {2,}.*|$))+)', re.MULTILINE).sub(googledoc_sections, text)
258+
return text
220259

221260
@staticmethod
222261
def _admonition(match, module=None, limit_types=None):
@@ -302,22 +341,16 @@ def _directive_opts(text: str) -> dict:
302341
return dict(re.findall(r'^ *:([^:]+): *(.*)', text, re.MULTILINE))
303342

304343
@staticmethod
305-
def doctests(text,
306-
_indent_doctests=partial(
307-
re.compile(r'(?:^(?P<fence>```|~~~).*\n)?'
308-
r'(?:^>>>.*'
309-
r'(?:\n(?:(?:>>>|\.\.\.).*))*'
310-
r'(?:\n.*)?\n\n?)+'
311-
r'(?P=fence)?', re.MULTILINE).sub,
312-
lambda m: (m.group(0) if m.group('fence') else
313-
('\n ' + '\n '.join(m.group(0).split('\n')) + '\n\n')))):
344+
def doctests(text):
314345
"""
315-
Indent non-fenced (`~~~`) top-level (0-indented)
316-
doctest blocks so they render as code.
346+
Fence non-fenced (`~~~`) top-level (0-indented)
347+
doctest blocks so they render as Python code.
317348
"""
318-
if not text.endswith('\n'): # Needed for the r'(?:\n.*)?\n\n?)+' line (GH-72)
319-
text += '\n'
320-
return _indent_doctests(text)
349+
with _fenced_code_blocks_hidden(text) as result:
350+
result[0] = re.compile(r'^(?:>>> .*)(?:\n.+)*', re.MULTILINE).sub(
351+
lambda match: '```python\n' + match.group() + '\n```\n', result[0])
352+
text = result[0]
353+
return text
321354

322355
@staticmethod
323356
def raw_urls(text):
@@ -387,13 +420,13 @@ def to_markdown(text: str, docformat: str = 'numpy,google', *,
387420
if 'google' in docformat:
388421
text = _ToMarkdown.google(text)
389422

423+
text = _ToMarkdown.doctests(text)
424+
390425
# If doing both, do numpy after google, otherwise google-style's
391426
# headings are incorrectly interpreted as numpy params
392427
if 'numpy' in docformat:
393428
text = _ToMarkdown.numpy(text)
394429

395-
text = _ToMarkdown.doctests(text)
396-
397430
if module and link:
398431
text = _code_refs(partial(_linkify, link=link, module=module, fmt='`{}`'), text)
399432

pdoc/test/__init__.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,8 +1056,9 @@ def test_google(self):
10561056
</dl>
10571057
<h2 id="examples">Examples</h2>
10581058
<p>Examples in doctest format.</p>
1059-
<pre><code>&gt;&gt;&gt; a = [1,2,3]
1059+
<pre><code class="python">&gt;&gt;&gt; a = [1,2,3]
10601060
</code></pre>
1061+
10611062
<h2 id="todos">Todos</h2>
10621063
<ul>
10631064
<li>For module TODOs</li>
@@ -1069,16 +1070,33 @@ def test_google(self):
10691070
def test_doctests(self):
10701071
expected = '''<p>Need an intro paragrapgh.</p>
10711072
<pre><code>&gt;&gt;&gt; Then code is indented one level
1073+
line1
1074+
line2
10721075
</code></pre>
10731076
<p>Alternatively</p>
1074-
<pre><code>fenced code works
1077+
<pre><code>&gt;&gt;&gt; doctest
1078+
fenced code works
1079+
always
10751080
</code></pre>
10761081
10771082
<h2 id="examples">Examples</h2>
1078-
<pre><code>&gt;&gt;&gt; nbytes(100)
1083+
<pre><code class="python">&gt;&gt;&gt; nbytes(100)
10791084
'100.0 bytes'
1085+
line2
1086+
</code></pre>
1087+
1088+
<p>some text</p>
1089+
<p>some text</p>
1090+
<pre><code class="python">&gt;&gt;&gt; another doctest
1091+
line1
1092+
line2
1093+
</code></pre>
10801094
1081-
&gt;&gt;&gt; asdf
1095+
<h2 id="example">Example</h2>
1096+
<pre><code class="python">&gt;&gt;&gt; f()
1097+
Traceback (most recent call last):
1098+
...
1099+
Exception: something went wrong
10821100
</code></pre>'''
10831101
text = inspect.getdoc(self._docmodule.doctests)
10841102
html = to_html(text, module=self._module, link=self._link)

pdoc/test/example_pkg/__init__.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,17 +249,34 @@ def doctests(self):
249249
Need an intro paragrapgh.
250250
251251
>>> Then code is indented one level
252+
line1
253+
line2
252254
253255
Alternatively
254256
```
257+
>>> doctest
255258
fenced code works
259+
always
256260
```
257261
258262
Examples:
259263
>>> nbytes(100)
260264
'100.0 bytes'
265+
line2
261266
262-
>>> asdf
267+
some text
268+
269+
some text
270+
271+
>>> another doctest
272+
line1
273+
line2
274+
275+
Example:
276+
>>> f()
277+
Traceback (most recent call last):
278+
...
279+
Exception: something went wrong
263280
"""
264281

265282
def reST_directives(self):

0 commit comments

Comments
 (0)