1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
|
#!/usr/bin/env python3
"""Update the ``help()`` outputs in ``docs/api.rst``."""
import contextlib
import difflib
import io
import operator
import pathlib
import re
import sys
import typing
import graphviz
SELF = pathlib.Path(__file__)
ALL_CLASSES = [graphviz.Graph, graphviz.Digraph, graphviz.Source]
ARGS_LINE = re.compile(r'(?:class | \| {2})\w+\(')
WRAP_AFTER = 80
INDENT = ' ' * 4
TARGET = pathlib.Path('docs/api.rst')
PATTERN_TMPL = (r'''
(
\ {{4}}>>>\ help\(graphviz\.{cls_name}\).*\n)
\ {{4}}Help\ on\ class\ {cls_name}
\ in\ module\ graphviz\.(?:graphs|sources):\n
\ {{4}}<BLANKLINE>\n
(?:.*\n)+?
\ {{4}}<BLANKLINE>\n
''')
IO_KWARGS = {'encoding': 'utf-8'}
def get_help(obj) -> str:
print(f'capture help() output for {obj}')
with io.StringIO() as buf:
with contextlib.redirect_stdout(buf):
help(obj)
buf.seek(0)
return ''.join(iterlines(buf))
def rpartition_initial(value: str, *, sep: str) -> typing.Tuple[str, str, str]:
"""Return (value, '', '') if sep not in value else value.rpartition(sep)."""
_, sep_found, _ = parts = value.rpartition(sep)
return tuple(reversed(parts)) if not sep_found else parts
def iterarguments(unwrapped_line: str) -> typing.Iterator[str]:
"""Yield unwrapped line of argument definitions divided into one line per arg.
>>> list(iterarguments('spam: str, eggs: typing.Union[str, None], ham'))
['spam: str,', 'eggs: typing.Union[str, None],', 'ham']
"""
pos = 0
bracket_level = paren_level = 0
for i, char in enumerate(unwrapped_line):
if char == '[':
bracket_level += 1
elif char == ']':
bracket_level -= 1
elif char == '(':
paren_level += 1
elif char == ')':
paren_level -= 1
elif (bracket_level == 0 and paren_level == 0 and char == ','
and unwrapped_line[i + 1: i + 3].strip() != '*'):
pos_including_comma = i + 1
yield unwrapped_line[pos:pos_including_comma].lstrip()
pos = pos_including_comma
yield unwrapped_line[pos:].lstrip()
def iterlines(stdout_lines, *,
line_indent: str = INDENT,
wrap_after: int = WRAP_AFTER) -> typing.Iterator[str]:
"""Yield post-processed help() stdout lines: rstrip, indent, wrap."""
for line in stdout_lines:
line = line.rstrip() + '\n'
line = line.replace("``'\\n'``", r"``'\\n'``")
if len(line) > wrap_after and ARGS_LINE.match(line):
indent = line_indent + ' ' * (line.index('(') + 1)
*start, rest = line.partition('(')
argument_line, *rest = rpartition_initial(rest, sep=' -> ')
arguments = list(iterarguments(argument_line))
print(len(line), 'character line wrapped into',
len(arguments), 'lines')
assert len(arguments) > 1, 'wrapped long argument line'
line = ''.join(start + [f'\n{indent}'.join(arguments)] + rest)
yield line_indent + line
print('run', [SELF.name] + sys.argv[1:])
help_docs = {cls.__name__: get_help(cls) for cls in ALL_CLASSES}
print('read', TARGET)
target = target_before = TARGET.read_text(**IO_KWARGS)
for cls_name, doc in help_docs.items():
print('replace', cls_name, 'PATTERN_TMPL match')
pattern = re.compile(PATTERN_TMPL.format(cls_name=cls_name), flags=re.VERBOSE)
target, found = pattern.subn(fr'\1{doc}', target, count=1)
assert found, f'replaced {cls_name} section'
target = target.replace(INDENT + '\n', INDENT + '<BLANKLINE>\n')
if target == target_before:
print(f'PASSED: unchanged {TARGET} (OK)')
sys.exit(None)
else:
print('write', TARGET)
splitlines = operator.methodcaller('splitlines', keepends=True)
target_before, target = map(splitlines, (target_before, target))
print(len(target_before), 'lines before')
print(len(target), 'lines after')
with TARGET.open('w', **IO_KWARGS) as f:
for line in target:
f.write(line)
for diff in difflib.context_diff(target_before, target):
print(diff)
message = f'FAILED: changed {TARGET!r} (WARNING)'
print(f'sys.exit({message!r})')
sys.exit(message)
|