|
5 | 5 | import os |
6 | 6 | import re |
7 | 7 | import subprocess |
| 8 | +import textwrap |
8 | 9 | import traceback |
| 10 | +from contextlib import contextmanager |
9 | 11 | from functools import partial, lru_cache |
10 | 12 | from typing import Callable, Match |
11 | 13 | from warnings import warn |
@@ -91,6 +93,32 @@ def glimpse(text: str, max_length=153, *, paragraph=True, |
91 | 93 | ) |
92 | 94 |
|
93 | 95 |
|
| 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 | + |
94 | 122 | class _ToMarkdown: |
95 | 123 | """ |
96 | 124 | This class serves as a namespace for methods converting common |
@@ -157,17 +185,19 @@ def _numpy_sections(match): |
157 | 185 | lists. |
158 | 186 | """ |
159 | 187 | section, body = match.groups() |
160 | | - if section.title() == 'See Also': |
| 188 | + section = section.title() |
| 189 | + if section == 'See Also': |
161 | 190 | body = re.sub(r'\n\s{4}\s*', ' ', body) # Handle line continuation |
162 | 191 | body = re.sub(r'^((?:\n?[\w.]* ?: .*)+)|(.*\w.*)', |
163 | 192 | _ToMarkdown._numpy_seealso, body) |
164 | | - elif section.title() in ('Returns', 'Yields', 'Raises', 'Warns'): |
| 193 | + elif section in ('Returns', 'Yields', 'Raises', 'Warns'): |
165 | 194 | body = re.sub(r'^(?:(?P<name>\*{0,2}\w+(?:, \*{0,2}\w+)*)' |
166 | 195 | r'(?: ?: (?P<type>.*))|' |
167 | 196 | r'(?P<just_type>\w[^\n`*]*))(?<!\.)$' |
168 | 197 | r'(?P<desc>(?:\n(?: {4}.*|$))*)', |
169 | 198 | _ToMarkdown._numpy_params, body, flags=re.MULTILINE) |
170 | | - else: |
| 199 | + elif section in ('Parameters', 'Receives', 'Other Parameters', |
| 200 | + 'Arguments', 'Args', 'Attributes'): |
171 | 201 | name = r'(?:\w|\{\w+(?:,\w+)+\})+' # Support curly brace expansion |
172 | 202 | body = re.sub(r'^(?P<name>\*{0,2}' + name + r'(?:, \*{0,2}' + name + r')*)' |
173 | 203 | r'(?: ?: (?P<type>.*))?(?<!\.)$' |
@@ -203,20 +233,29 @@ def indent(indent, text, *, clean_first=False): |
203 | 233 | return re.sub(r'\n', '\n' + indent, indent + text.rstrip()) |
204 | 234 |
|
205 | 235 | @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): |
215 | 237 | """ |
216 | 238 | Convert `text` in Google-style docstring format to Markdown |
217 | 239 | to be further converted later. |
218 | 240 | """ |
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 |
220 | 259 |
|
221 | 260 | @staticmethod |
222 | 261 | def _admonition(match, module=None, limit_types=None): |
@@ -302,22 +341,16 @@ def _directive_opts(text: str) -> dict: |
302 | 341 | return dict(re.findall(r'^ *:([^:]+): *(.*)', text, re.MULTILINE)) |
303 | 342 |
|
304 | 343 | @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): |
314 | 345 | """ |
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. |
317 | 348 | """ |
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 |
321 | 354 |
|
322 | 355 | @staticmethod |
323 | 356 | def raw_urls(text): |
@@ -387,13 +420,13 @@ def to_markdown(text: str, docformat: str = 'numpy,google', *, |
387 | 420 | if 'google' in docformat: |
388 | 421 | text = _ToMarkdown.google(text) |
389 | 422 |
|
| 423 | + text = _ToMarkdown.doctests(text) |
| 424 | + |
390 | 425 | # If doing both, do numpy after google, otherwise google-style's |
391 | 426 | # headings are incorrectly interpreted as numpy params |
392 | 427 | if 'numpy' in docformat: |
393 | 428 | text = _ToMarkdown.numpy(text) |
394 | 429 |
|
395 | | - text = _ToMarkdown.doctests(text) |
396 | | - |
397 | 430 | if module and link: |
398 | 431 | text = _code_refs(partial(_linkify, link=link, module=module, fmt='`{}`'), text) |
399 | 432 |
|
|
0 commit comments