Clean up docstring parsing and move it into qutebrowser for commands.
This commit is contained in:
parent
b5f28b6ff2
commit
b453ae563e
@ -137,6 +137,7 @@ class register: # pylint: disable=invalid-name
|
|||||||
self.ignore_args = ignore_args
|
self.ignore_args = ignore_args
|
||||||
self.parser = None
|
self.parser = None
|
||||||
self.func = None
|
self.func = None
|
||||||
|
self.docparser = None
|
||||||
if modes is not None:
|
if modes is not None:
|
||||||
for m in modes:
|
for m in modes:
|
||||||
if not isinstance(m, usertypes.KeyMode):
|
if not isinstance(m, usertypes.KeyMode):
|
||||||
@ -166,10 +167,13 @@ class register: # pylint: disable=invalid-name
|
|||||||
for name in names:
|
for name in names:
|
||||||
if name in cmd_dict:
|
if name in cmd_dict:
|
||||||
raise ValueError("{} is already registered!".format(name))
|
raise ValueError("{} is already registered!".format(name))
|
||||||
self.parser = argparser.ArgumentParser(names[0])
|
self.docparser = utils.DocstringParser(func)
|
||||||
|
self.parser = argparser.ArgumentParser(
|
||||||
|
names[0], description=self.docparser.short_desc,
|
||||||
|
epilog=self.docparser.long_desc)
|
||||||
self.parser.add_argument('-h', '--help', action=argparser.HelpAction,
|
self.parser.add_argument('-h', '--help', action=argparser.HelpAction,
|
||||||
default=argparser.SUPPRESS, nargs=0,
|
default=argparser.SUPPRESS, nargs=0,
|
||||||
help="Show this help message.")
|
help=argparser.SUPPRESS)
|
||||||
has_count, desc, type_conv = self._inspect_func()
|
has_count, desc, type_conv = self._inspect_func()
|
||||||
cmd = command.Command(
|
cmd = command.Command(
|
||||||
name=names[0], split=self.split, hide=self.hide, count=has_count,
|
name=names[0], split=self.split, hide=self.hide, count=has_count,
|
||||||
@ -267,7 +271,13 @@ class register: # pylint: disable=invalid-name
|
|||||||
annotation_info: An AnnotationInfo tuple for the parameter.
|
annotation_info: An AnnotationInfo tuple for the parameter.
|
||||||
"""
|
"""
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
kwargs['help'] = self.docparser.arg_descs[param.name]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
typ = self._get_type(param, annotation_info)
|
typ = self._get_type(param, annotation_info)
|
||||||
|
|
||||||
if isinstance(typ, tuple):
|
if isinstance(typ, tuple):
|
||||||
pass
|
pass
|
||||||
elif utils.is_enum(typ):
|
elif utils.is_enum(typ):
|
||||||
@ -282,6 +292,7 @@ class register: # pylint: disable=invalid-name
|
|||||||
elif typ is not bool and param.default is not inspect.Parameter.empty:
|
elif typ is not bool and param.default is not inspect.Parameter.empty:
|
||||||
kwargs['default'] = param.default
|
kwargs['default'] = param.default
|
||||||
kwargs['nargs'] = '?'
|
kwargs['nargs'] = '?'
|
||||||
|
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def _parse_annotation(self, param):
|
def _parse_annotation(self, param):
|
||||||
|
@ -21,9 +21,11 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import io
|
import io
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import enum
|
import enum
|
||||||
import shlex
|
import shlex
|
||||||
|
import inspect
|
||||||
import os.path
|
import os.path
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
@ -36,7 +38,7 @@ from PyQt5.QtGui import QKeySequence, QColor
|
|||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
||||||
import qutebrowser
|
import qutebrowser
|
||||||
from qutebrowser.utils import qtutils
|
from qutebrowser.utils import usertypes, qtutils
|
||||||
|
|
||||||
|
|
||||||
def elide(text, length):
|
def elide(text, length):
|
||||||
@ -503,3 +505,92 @@ def is_enum(obj):
|
|||||||
return issubclass(obj, enum.Enum)
|
return issubclass(obj, enum.Enum)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class DocstringParser:
|
||||||
|
|
||||||
|
"""Generate documentation based on a docstring of a command handler.
|
||||||
|
|
||||||
|
The docstring needs to follow the format described in HACKING.
|
||||||
|
"""
|
||||||
|
|
||||||
|
State = usertypes.enum('State', 'short', 'desc', 'desc_hidden',
|
||||||
|
'arg_start', 'arg_inside', 'misc')
|
||||||
|
|
||||||
|
def __init__(self, func):
|
||||||
|
"""Constructor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func: The function to parse the docstring for.
|
||||||
|
"""
|
||||||
|
self.state = self.State.short
|
||||||
|
self.short_desc = []
|
||||||
|
self.long_desc = []
|
||||||
|
self.arg_descs = collections.OrderedDict()
|
||||||
|
self.cur_arg_name = None
|
||||||
|
self.handlers = {
|
||||||
|
self.State.short: self._parse_short,
|
||||||
|
self.State.desc: self._parse_desc,
|
||||||
|
self.State.desc_hidden: self._skip,
|
||||||
|
self.State.arg_start: self._parse_arg_start,
|
||||||
|
self.State.arg_inside: self._parse_arg_inside,
|
||||||
|
self.State.misc: self._skip,
|
||||||
|
}
|
||||||
|
doc = inspect.getdoc(func)
|
||||||
|
for line in doc.splitlines():
|
||||||
|
handler = self.handlers[self.state]
|
||||||
|
stop = handler(line)
|
||||||
|
if stop:
|
||||||
|
break
|
||||||
|
for k, v in self.arg_descs.items():
|
||||||
|
self.arg_descs[k] = ' '.join(v)
|
||||||
|
self.long_desc = ' '.join(self.long_desc)
|
||||||
|
self.short_desc = ' '.join(self.short_desc)
|
||||||
|
|
||||||
|
def _process_arg(self, line):
|
||||||
|
"""Helper method to process a line like 'fooarg: Blah blub'."""
|
||||||
|
self.cur_arg_name, argdesc = line.split(':', maxsplit=1)
|
||||||
|
self.cur_arg_name = self.cur_arg_name.strip().lstrip('*')
|
||||||
|
self.arg_descs[self.cur_arg_name] = [argdesc.strip()]
|
||||||
|
|
||||||
|
def _skip(self, line):
|
||||||
|
"""Handler to ignore everything until we get 'Args:'."""
|
||||||
|
if line.startswith('Args:'):
|
||||||
|
self.state = self.State.arg_start
|
||||||
|
|
||||||
|
def _parse_short(self, line):
|
||||||
|
"""Parse the short description (first block) in the docstring."""
|
||||||
|
if not line:
|
||||||
|
self.state = self.State.desc
|
||||||
|
else:
|
||||||
|
self.short_desc.append(line.strip())
|
||||||
|
|
||||||
|
def _parse_desc(self, line):
|
||||||
|
"""Parse the long description in the docstring."""
|
||||||
|
if line.startswith('Args:'):
|
||||||
|
self.state = self.State.arg_start
|
||||||
|
elif line.startswith('Emit:') or line.startswith('Raise:'):
|
||||||
|
self.state = self.State.misc
|
||||||
|
elif line.strip() == '//':
|
||||||
|
self.state = self.State.desc_hidden
|
||||||
|
elif line.strip():
|
||||||
|
self.long_desc.append(line.strip())
|
||||||
|
|
||||||
|
def _parse_arg_start(self, line):
|
||||||
|
"""Parse first argument line."""
|
||||||
|
self._process_arg(line)
|
||||||
|
self.state = self.State.arg_inside
|
||||||
|
|
||||||
|
def _parse_arg_inside(self, line):
|
||||||
|
"""Parse subsequent argument lines."""
|
||||||
|
argname = self.cur_arg_name
|
||||||
|
if re.match(r'^[A-Z][a-z]+:$', line):
|
||||||
|
if not self.arg_descs[argname][-1].strip():
|
||||||
|
self.arg_descs[argname] = self.arg_descs[argname][:-1]
|
||||||
|
return True
|
||||||
|
elif not line.strip():
|
||||||
|
self.arg_descs[argname].append('\n\n')
|
||||||
|
elif line[4:].startswith(' '):
|
||||||
|
self.arg_descs[argname].append(line.strip() + '\n')
|
||||||
|
else:
|
||||||
|
self._process_arg(line)
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
|
|
||||||
"""Generate asciidoc source for qutebrowser based on docstrings."""
|
"""Generate asciidoc source for qutebrowser based on docstrings."""
|
||||||
|
|
||||||
import re
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import html
|
import html
|
||||||
@ -38,7 +37,7 @@ import qutebrowser.app
|
|||||||
from qutebrowser import qutebrowser as qutequtebrowser
|
from qutebrowser import qutebrowser as qutequtebrowser
|
||||||
from qutebrowser.commands import cmdutils
|
from qutebrowser.commands import cmdutils
|
||||||
from qutebrowser.config import configdata
|
from qutebrowser.config import configdata
|
||||||
from qutebrowser.utils import usertypes
|
from qutebrowser.utils import utils
|
||||||
|
|
||||||
|
|
||||||
def _open_file(name, mode='w'):
|
def _open_file(name, mode='w'):
|
||||||
@ -46,75 +45,6 @@ def _open_file(name, mode='w'):
|
|||||||
return open(name, mode, newline='\n', encoding='utf-8')
|
return open(name, mode, newline='\n', encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
def _parse_docstring(func): # noqa
|
|
||||||
"""Generate documentation based on a docstring of a command handler.
|
|
||||||
|
|
||||||
The docstring needs to follow the format described in HACKING.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
func: The function to generate the docstring for.
|
|
||||||
|
|
||||||
Return:
|
|
||||||
A (short_desc, long_desc, arg_descs) tuple.
|
|
||||||
"""
|
|
||||||
# pylint: disable=too-many-branches
|
|
||||||
State = usertypes.enum('State', 'short', # pylint: disable=invalid-name
|
|
||||||
'desc', 'desc_hidden', 'arg_start', 'arg_inside',
|
|
||||||
'misc')
|
|
||||||
doc = inspect.getdoc(func)
|
|
||||||
lines = doc.splitlines()
|
|
||||||
|
|
||||||
cur_state = State.short
|
|
||||||
|
|
||||||
short_desc = []
|
|
||||||
long_desc = []
|
|
||||||
arg_descs = collections.OrderedDict()
|
|
||||||
cur_arg_name = None
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
if cur_state == State.short:
|
|
||||||
if not line:
|
|
||||||
cur_state = State.desc
|
|
||||||
else:
|
|
||||||
short_desc.append(line.strip())
|
|
||||||
elif cur_state == State.desc:
|
|
||||||
if line.startswith('Args:'):
|
|
||||||
cur_state = State.arg_start
|
|
||||||
elif line.startswith('Emit:') or line.startswith('Raise:'):
|
|
||||||
cur_state = State.misc
|
|
||||||
elif line.strip() == '//':
|
|
||||||
cur_state = State.desc_hidden
|
|
||||||
elif line.strip():
|
|
||||||
long_desc.append(line.strip())
|
|
||||||
elif cur_state == State.misc:
|
|
||||||
if line.startswith('Args:'):
|
|
||||||
cur_state = State.arg_start
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
elif cur_state == State.desc_hidden:
|
|
||||||
if line.startswith('Args:'):
|
|
||||||
cur_state = State.arg_start
|
|
||||||
elif cur_state == State.arg_start:
|
|
||||||
cur_arg_name, argdesc = line.split(':', maxsplit=1)
|
|
||||||
cur_arg_name = cur_arg_name.strip().lstrip('*')
|
|
||||||
arg_descs[cur_arg_name] = [argdesc.strip()]
|
|
||||||
cur_state = State.arg_inside
|
|
||||||
elif cur_state == State.arg_inside:
|
|
||||||
if re.match('^[A-Z][a-z]+:$', line):
|
|
||||||
if not arg_descs[cur_arg_name][-1].strip():
|
|
||||||
arg_descs[cur_arg_name] = arg_descs[cur_arg_name][:-1]
|
|
||||||
break
|
|
||||||
elif not line.strip():
|
|
||||||
arg_descs[cur_arg_name].append('\n\n')
|
|
||||||
elif line[4:].startswith(' '):
|
|
||||||
arg_descs[cur_arg_name].append(line.strip() + '\n')
|
|
||||||
else:
|
|
||||||
cur_arg_name, argdesc = line.split(':', maxsplit=1)
|
|
||||||
cur_arg_name = cur_arg_name.strip().lstrip('*')
|
|
||||||
arg_descs[cur_arg_name] = [argdesc.strip()]
|
|
||||||
return (short_desc, long_desc, arg_descs)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_cmd_syntax(name, cmd):
|
def _get_cmd_syntax(name, cmd):
|
||||||
"""Get the command syntax for a command."""
|
"""Get the command syntax for a command."""
|
||||||
words = []
|
words = []
|
||||||
@ -179,14 +109,14 @@ def _get_command_doc(name, cmd):
|
|||||||
if syntax != name:
|
if syntax != name:
|
||||||
output.append('Syntax: +:{}+'.format(syntax))
|
output.append('Syntax: +:{}+'.format(syntax))
|
||||||
output.append("")
|
output.append("")
|
||||||
short_desc, long_desc, arg_descs = _parse_docstring(cmd.handler)
|
parser = utils.DocstringParser(cmd.handler)
|
||||||
output.append(' '.join(short_desc))
|
output.append(parser.short_desc)
|
||||||
output.append("")
|
output.append("")
|
||||||
output.append(' '.join(long_desc))
|
output.append(parser.long_desc)
|
||||||
if arg_descs:
|
if parser.arg_descs:
|
||||||
output.append("")
|
output.append("")
|
||||||
for arg, desc in arg_descs.items():
|
for arg, desc in parser.arg_descs.items():
|
||||||
text = ' '.join(desc).splitlines()
|
text = desc.splitlines()
|
||||||
firstline = text[0].replace(', or None', '')
|
firstline = text[0].replace(', or None', '')
|
||||||
item = "* +{}+: {}".format(arg, firstline)
|
item = "* +{}+: {}".format(arg, firstline)
|
||||||
if arg in defaults:
|
if arg in defaults:
|
||||||
|
Loading…
Reference in New Issue
Block a user