|
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