qutebrowser/scripts/dev/src2asciidoc.py

569 lines
20 KiB
Python
Raw Normal View History

2014-09-22 20:21:00 +02:00
#!/usr/bin/env python3
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2014-08-02 21:13:14 +02:00
2019-02-23 06:34:17 +01:00
# Copyright 2014-2019 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
2014-05-28 16:48:19 +02:00
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Generate asciidoc source for qutebrowser based on docstrings."""
import os
2017-12-15 23:08:53 +01:00
import os.path
2014-05-28 16:48:19 +02:00
import sys
import shutil
2014-05-28 16:48:19 +02:00
import inspect
import subprocess
2014-08-26 19:10:14 +02:00
import tempfile
2014-09-07 20:11:38 +02:00
import argparse
2014-05-28 16:48:19 +02:00
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
2016-04-27 18:30:54 +02:00
os.pardir))
2014-05-28 16:48:19 +02:00
2014-07-17 21:35:27 +02:00
# We import qutebrowser.app so all @cmdutils-register decorators are run.
2014-08-04 03:47:09 +02:00
import qutebrowser.app
2016-08-16 13:31:53 +02:00
from qutebrowser import qutebrowser, commands
2018-12-10 09:38:23 +01:00
from qutebrowser.extensions import loader
from qutebrowser.commands import argparser
from qutebrowser.config import configdata, configtypes
2016-08-03 11:35:08 +02:00
from qutebrowser.utils import docutils, usertypes
from qutebrowser.misc import objects
2017-12-15 23:08:53 +01:00
from scripts import asciidoc2html, utils
2014-05-28 16:48:19 +02:00
FILE_HEADER = """
// DO NOT EDIT THIS FILE DIRECTLY!
2017-07-21 13:18:20 +02:00
// It is autogenerated by running:
// $ python3 scripts/dev/src2asciidoc.py
// vim: readonly:
""".lstrip()
2014-07-17 21:35:27 +02:00
2014-09-07 20:11:38 +02:00
class UsageFormatter(argparse.HelpFormatter):
"""Patched HelpFormatter to include some asciidoc markup in the usage.
This does some horrible things, but the alternative would be to reimplement
argparse.HelpFormatter while copying 99% of the code :-/
"""
def _format_usage(self, usage, actions, groups, _prefix):
"""Override _format_usage to not add the 'usage:' prefix."""
return super()._format_usage(usage, actions, groups, '')
def _get_default_metavar_for_optional(self, action):
"""Do name transforming when getting metavar."""
return argparser.arg_name(action.dest.upper())
def _get_default_metavar_for_positional(self, action):
"""Do name transforming when getting metavar."""
return argparser.arg_name(action.dest)
2014-09-07 20:11:38 +02:00
def _metavar_formatter(self, action, default_metavar):
"""Override _metavar_formatter to add asciidoc markup to metavars.
Most code here is copied from Python 3.4's argparse.py.
"""
if action.metavar is not None:
result = "'{}'".format(action.metavar)
elif action.choices is not None:
choice_strs = [str(choice) for choice in action.choices]
result = ('{' + ','.join('*{}*'.format(e) for e in choice_strs) +
'}')
2014-09-07 20:11:38 +02:00
else:
result = "'{}'".format(default_metavar)
def fmt(tuple_size):
2014-09-08 07:44:32 +02:00
"""Format the result according to the tuple size."""
2014-09-07 20:11:38 +02:00
if isinstance(result, tuple):
return result
else:
return (result, ) * tuple_size
return fmt
def _format_actions_usage(self, actions, groups):
"""Override _format_actions_usage to add asciidoc markup to flags.
Because argparse.py's _format_actions_usage is very complex, we first
monkey-patch the option strings to include the asciidoc markup, then
run the original method, then undo the patching.
"""
old_option_strings = {}
for action in actions:
old_option_strings[action] = action.option_strings[:]
action.option_strings = ['*{}*'.format(s)
for s in action.option_strings]
ret = super()._format_actions_usage(actions, groups)
for action in actions:
action.option_strings = old_option_strings[action]
return ret
def _open_file(name, mode='w'):
"""Open a file with a preset newline/encoding mode."""
return open(name, mode, newline='\n', encoding='utf-8')
2014-05-28 16:48:19 +02:00
2014-07-17 21:35:27 +02:00
2014-09-08 07:44:32 +02:00
def _get_cmd_syntax(_name, cmd):
2014-09-07 20:11:38 +02:00
"""Get the command syntax for a command.
We monkey-patch the parser's formatter_class here to use our UsageFormatter
which adds some asciidoc markup.
"""
old_fmt_class = cmd.parser.formatter_class
cmd.parser.formatter_class = UsageFormatter
usage = cmd.parser.format_usage().rstrip()
cmd.parser.formatter_class = old_fmt_class
return usage
2014-05-28 17:47:11 +02:00
def _get_command_quickref(cmds):
2014-07-17 21:35:27 +02:00
"""Generate the command quick reference."""
out = []
2014-05-29 21:38:06 +02:00
out.append('[options="header",width="75%",cols="25%,75%"]')
out.append('|==============')
out.append('|Command|Description')
for name, cmd in cmds:
desc = inspect.getdoc(cmd.handler).splitlines()[0]
2018-11-28 13:01:43 +01:00
out.append('|<<{name},{name}>>|{desc}'.format(name=name, desc=desc))
out.append('|==============')
return '\n'.join(out)
def _get_setting_quickref():
2014-07-17 21:35:27 +02:00
"""Generate the settings quick reference."""
out = []
2017-07-01 22:58:50 +02:00
out.append('')
out.append('[options="header",width="75%",cols="25%,75%"]')
out.append('|==============')
out.append('|Setting|Description')
for opt in sorted(configdata.DATA.values()):
desc = opt.description.splitlines()[0]
out.append('|<<{},{}>>|{}'.format(opt.name, opt.name, desc))
out.append('|==============')
return '\n'.join(out)
def _get_configtypes():
"""Get configtypes classes to document."""
2017-10-24 09:37:10 +02:00
predicate = lambda e: (
inspect.isclass(e) and
# pylint: disable=protected-access
e not in [configtypes.BaseType, configtypes.MappingType,
configtypes._Numeric] and
# pylint: enable=protected-access
issubclass(e, configtypes.BaseType))
yield from inspect.getmembers(configtypes, predicate)
def _get_setting_types_quickref():
"""Generate the setting types quick reference."""
out = []
out.append('[[types]]')
out.append('[options="header",width="75%",cols="25%,75%"]')
out.append('|==============')
out.append('|Type|Description')
for name, typ in _get_configtypes():
parser = docutils.DocstringParser(typ)
desc = parser.short_desc
if parser.long_desc:
desc += '\n\n' + parser.long_desc
out.append('|{}|{}'.format(name, desc))
out.append('|==============')
return '\n'.join(out)
def _get_command_doc(name, cmd):
2014-07-17 21:35:27 +02:00
"""Generate the documentation for a command."""
2014-09-07 20:11:38 +02:00
output = ['[[{}]]'.format(name)]
2014-09-07 21:09:21 +02:00
output += ['=== {}'.format(name)]
syntax = _get_cmd_syntax(name, cmd)
2014-08-03 01:00:25 +02:00
if syntax != name:
output.append('Syntax: +:{}+'.format(syntax))
2014-09-07 20:11:38 +02:00
output.append("")
2014-09-23 04:22:51 +02:00
parser = docutils.DocstringParser(cmd.handler)
output.append(parser.short_desc)
2014-09-07 20:11:38 +02:00
if parser.long_desc:
output.append("")
output.append(parser.long_desc)
2014-09-07 21:04:39 +02:00
2015-04-06 19:48:36 +02:00
output += list(_get_command_doc_args(cmd, parser))
output += list(_get_command_doc_count(cmd, parser))
output += list(_get_command_doc_notes(cmd))
output.append("")
output.append("")
return '\n'.join(output)
def _get_command_doc_args(cmd, parser):
"""Get docs for the arguments of a command.
Args:
cmd: The Command to get the docs for.
parser: The DocstringParser to use.
Yield:
Strings which should be added to the docs.
"""
2014-09-07 21:04:39 +02:00
if cmd.pos_args:
2015-04-06 19:48:36 +02:00
yield ""
yield "==== positional arguments"
for arg, name in cmd.pos_args:
2014-09-07 21:04:39 +02:00
try:
2015-04-06 19:48:36 +02:00
yield "* +'{}'+: {}".format(name, parser.arg_descs[arg])
2014-09-07 21:04:39 +02:00
except KeyError as e:
raise KeyError("No description for arg {} of command "
"'{}'!".format(e, cmd.name)) from e
2014-09-07 21:04:39 +02:00
if cmd.opt_args:
2015-04-06 19:48:36 +02:00
yield ""
yield "==== optional arguments"
2014-09-07 21:04:39 +02:00
for arg, (long_flag, short_flag) in cmd.opt_args.items():
try:
2015-04-06 19:48:36 +02:00
yield '* +*{}*+, +*{}*+: {}'.format(short_flag, long_flag,
parser.arg_descs[arg])
except KeyError as e:
2014-09-07 21:04:39 +02:00
raise KeyError("No description for arg {} of command "
"'{}'!".format(e, cmd.name)) from e
2014-09-07 21:04:39 +02:00
2015-04-06 19:48:36 +02:00
def _get_command_doc_count(cmd, parser):
"""Get docs for the count of a command.
Args:
cmd: The Command to get the docs for.
parser: The DocstringParser to use.
Yield:
Strings which should be added to the docs.
"""
2016-05-10 20:26:54 +02:00
for param in inspect.signature(cmd.handler).parameters.values():
2018-12-03 08:44:35 +01:00
if cmd.get_arg_info(param).value in cmd.COUNT_COMMAND_VALUES:
2016-05-10 20:26:54 +02:00
yield ""
yield "==== count"
try:
yield parser.arg_descs[param.name]
except KeyError:
try:
yield parser.arg_descs['count']
except KeyError as e:
raise KeyError("No description for count arg {!r} of "
"command {!r}!"
.format(param.name, cmd.name)) from e
2015-04-06 19:48:36 +02:00
def _get_command_doc_notes(cmd):
"""Get docs for the notes of a command.
Args:
cmd: The Command to get the docs for.
parser: The DocstringParser to use.
2014-09-13 00:33:15 +02:00
2015-04-06 19:48:36 +02:00
Yield:
Strings which should be added to the docs.
"""
if (cmd.maxsplit is not None or cmd.no_cmd_split or
cmd.no_replace_variables and cmd.name != "spawn"):
2015-04-06 19:48:36 +02:00
yield ""
yield "==== note"
if cmd.maxsplit is not None:
2015-04-06 19:48:36 +02:00
yield ("* This command does not split arguments after the last "
"argument and handles quotes literally.")
if cmd.no_cmd_split:
2015-04-06 19:48:36 +02:00
yield ("* With this command, +;;+ is interpreted literally "
"instead of splitting off a second command.")
if cmd.no_replace_variables and cmd.name != "spawn":
yield r"* This command does not replace variables like +\{url\}+."
2014-05-28 16:48:19 +02:00
def _get_action_metavar(action, nargs=1):
"""Get the metavar to display for an argparse action.
Args:
action: The argparse action to get the metavar for.
nargs: The nargs setting for the related argument.
"""
2014-07-24 00:38:23 +02:00
if action.metavar is not None:
if isinstance(action.metavar, str):
elems = [action.metavar] * nargs
else:
elems = action.metavar
return ' '.join("'{}'".format(e) for e in elems)
2014-07-24 00:38:23 +02:00
elif action.choices is not None:
choices = ','.join(str(e) for e in action.choices)
2014-07-24 00:38:23 +02:00
return "'{{{}}}'".format(choices)
else:
return "'{}'".format(action.dest.upper())
def _format_action_args(action):
"""Get an argument string based on an argparse action."""
if action.nargs is None:
return _get_action_metavar(action)
elif action.nargs == '?':
return '[{}]'.format(_get_action_metavar(action))
elif action.nargs == '*':
return '[{mv} [{mv} ...]]'.format(mv=_get_action_metavar(action))
elif action.nargs == '+':
return '{mv} [{mv} ...]'.format(mv=_get_action_metavar(action))
elif action.nargs == '...':
return '...'
else:
return _get_action_metavar(action, nargs=action.nargs)
2014-07-24 00:38:23 +02:00
def _format_action(action):
"""Get an invocation string/help from an argparse action."""
if action.help == argparse.SUPPRESS:
return None
2014-07-24 00:38:23 +02:00
if not action.option_strings:
invocation = '*{}*::'.format(_get_action_metavar(action))
else:
parts = []
if action.nargs == 0:
# Doesn't take a value, so the syntax is -s, --long
parts += ['*{}*'.format(s) for s in action.option_strings]
else:
# Takes a value, so the syntax is -s ARGS or --long ARGS.
args_string = _format_action_args(action)
for opt in action.option_strings:
parts.append('*{}* {}'.format(opt, args_string))
invocation = ', '.join(parts) + '::'
return '{}\n {}\n'.format(invocation, action.help)
2014-07-24 00:38:23 +02:00
2014-09-07 20:11:38 +02:00
def generate_commands(filename):
2014-07-17 21:35:27 +02:00
"""Generate the complete commands section."""
2014-09-07 20:11:38 +02:00
with _open_file(filename) as f:
f.write(FILE_HEADER)
2016-08-16 13:31:53 +02:00
f.write("= Commands\n\n")
f.write(commands.__doc__)
2014-09-07 20:11:38 +02:00
normal_cmds = []
other_cmds = []
2014-09-07 20:11:38 +02:00
debug_cmds = []
for name, cmd in objects.commands.items():
if cmd.deprecated:
continue
if usertypes.KeyMode.normal not in cmd.modes:
other_cmds.append((name, cmd))
2014-09-07 20:11:38 +02:00
elif cmd.debug:
debug_cmds.append((name, cmd))
else:
2014-09-07 20:11:38 +02:00
normal_cmds.append((name, cmd))
normal_cmds.sort()
other_cmds.sort()
2014-09-07 20:11:38 +02:00
debug_cmds.sort()
f.write("\n")
2014-09-07 21:09:21 +02:00
f.write("== Normal commands\n")
2014-09-07 20:11:38 +02:00
f.write(".Quick reference\n")
2014-09-07 21:19:04 +02:00
f.write(_get_command_quickref(normal_cmds) + '\n')
2014-09-07 20:11:38 +02:00
for name, cmd in normal_cmds:
f.write(_get_command_doc(name, cmd))
f.write("\n")
f.write("== Commands not usable in normal mode\n")
2014-09-07 20:11:38 +02:00
f.write(".Quick reference\n")
f.write(_get_command_quickref(other_cmds) + '\n')
for name, cmd in other_cmds:
2014-09-07 20:11:38 +02:00
f.write(_get_command_doc(name, cmd))
f.write("\n")
2014-09-07 21:09:21 +02:00
f.write("== Debugging commands\n")
2014-09-07 20:11:38 +02:00
f.write("These commands are mainly intended for debugging. They are "
"hidden if qutebrowser was started without the "
"`--debug`-flag.\n")
f.write("\n")
f.write(".Quick reference\n")
2014-09-07 21:19:04 +02:00
f.write(_get_command_quickref(debug_cmds) + '\n')
2014-09-07 20:11:38 +02:00
for name, cmd in debug_cmds:
f.write(_get_command_doc(name, cmd))
2017-07-04 13:05:10 +02:00
def _generate_setting_backend_info(f, opt):
"""Generate backend information for the given option."""
2017-07-04 13:05:10 +02:00
all_backends = [usertypes.Backend.QtWebKit, usertypes.Backend.QtWebEngine]
if opt.raw_backends is not None:
for name, conditional in sorted(opt.raw_backends.items()):
if conditional is True:
pass
elif conditional is False:
f.write("\nOn {}, this setting is unavailable.\n".format(name))
else:
f.write("\nOn {}, this setting requires {} or newer.\n"
.format(name, conditional))
elif opt.backends == all_backends:
pass
elif opt.backends == [usertypes.Backend.QtWebKit]:
f.write("\nThis setting is only available with the QtWebKit "
"backend.\n")
elif opt.backends == [usertypes.Backend.QtWebEngine]:
f.write("\nThis setting is only available with the QtWebEngine "
"backend.\n")
else:
raise ValueError("Invalid value {!r} for opt.backends"
.format(opt.backends))
2017-07-01 22:58:50 +02:00
def _generate_setting_option(f, opt):
"""Generate documentation for a single section."""
2017-07-01 22:58:50 +02:00
f.write("\n")
f.write('[[{}]]'.format(opt.name) + "\n")
f.write("=== {}".format(opt.name) + "\n")
2017-07-01 22:58:50 +02:00
f.write(opt.description + "\n")
if opt.restart:
f.write("This setting requires a restart.\n")
if opt.supports_pattern:
f.write("\nThis setting supports URL patterns.\n")
if opt.no_autoconfig:
f.write("\nThis setting can only be set in config.py.\n")
2017-07-01 22:58:50 +02:00
f.write("\n")
typ = opt.typ.get_name().replace(',', '&#44;')
f.write('Type: <<types,{typ}>>\n'.format(typ=typ))
f.write("\n")
2017-07-01 22:58:50 +02:00
valid_values = opt.typ.get_valid_values()
2017-10-04 11:46:42 +02:00
if valid_values is not None and valid_values.generate_docs:
2017-07-01 22:58:50 +02:00
f.write("Valid values:\n")
f.write("\n")
2017-07-01 22:58:50 +02:00
for val in valid_values:
try:
desc = valid_values.descriptions[val]
f.write(" * +{}+: {}".format(val, desc) + "\n")
except KeyError:
f.write(" * +{}+".format(val) + "\n")
f.write("\n")
2016-08-03 11:35:08 +02:00
f.write("Default: {}\n".format(opt.typ.to_doc(opt.default)))
2017-07-04 13:05:10 +02:00
_generate_setting_backend_info(f, opt)
2016-08-03 11:35:08 +02:00
2014-09-07 20:11:38 +02:00
def generate_settings(filename):
2014-07-17 21:35:27 +02:00
"""Generate the complete settings section."""
2017-07-01 22:58:50 +02:00
configdata.init()
2014-09-07 20:11:38 +02:00
with _open_file(filename) as f:
f.write(FILE_HEADER)
f.write("= Setting reference\n\n")
f.write("== All settings\n")
2014-09-07 20:11:38 +02:00
f.write(_get_setting_quickref() + "\n")
2017-07-01 22:58:50 +02:00
for opt in sorted(configdata.DATA.values()):
_generate_setting_option(f, opt)
f.write("\n== Setting types\n")
f.write(_get_setting_types_quickref() + "\n")
2014-05-28 19:12:12 +02:00
2014-09-05 07:45:47 +02:00
def _format_block(filename, what, data):
"""Format a block in a file.
2014-07-24 01:51:23 +02:00
2014-09-05 07:45:47 +02:00
The block is delimited by markers like these:
// QUTE_*_START
...
// QUTE_*_END
2014-07-24 01:51:23 +02:00
2014-09-05 07:45:47 +02:00
The * part is the part which should be given as 'what'.
Args:
filename: The file to change.
what: What to change (authors, options, etc.)
data; A list of strings which is the new data.
"""
what = what.upper()
oshandle, tmpname = tempfile.mkstemp()
try:
with _open_file(filename, mode='r') as infile, \
_open_file(oshandle, mode='w') as temp:
found_start = False
found_end = False
for line in infile:
if line.strip() == '// QUTE_{}_START'.format(what):
temp.write(line)
temp.write(''.join(data))
found_start = True
elif line.strip() == '// QUTE_{}_END'.format(what.upper()):
temp.write(line)
found_end = True
elif (not found_start) or found_end:
temp.write(line)
if not found_start:
raise Exception("Marker '// QUTE_{}_START' not found in "
"'{}'!".format(what, filename))
elif not found_end:
raise Exception("Marker '// QUTE_{}_END' not found in "
"'{}'!".format(what, filename))
2015-03-11 20:14:39 +01:00
except:
2014-09-05 07:45:47 +02:00
os.remove(tmpname)
raise
else:
os.remove(filename)
shutil.move(tmpname, filename)
2014-07-24 01:51:23 +02:00
2014-09-05 07:45:47 +02:00
def regenerate_manpage(filename):
"""Update manpage OPTIONS using an argparse parser."""
2014-09-08 07:44:32 +02:00
parser = qutebrowser.get_argparser()
groups = []
2014-09-05 07:45:47 +02:00
# positionals, optionals and user-defined groups
2017-10-24 08:50:07 +02:00
# pylint: disable=protected-access
2014-09-05 07:45:47 +02:00
for group in parser._action_groups:
groupdata = []
groupdata.append('=== {}'.format(group.title))
2014-09-05 07:45:47 +02:00
if group.description is not None:
groupdata.append(group.description)
2014-09-05 07:45:47 +02:00
for action in group._group_actions:
action_data = _format_action(action)
if action_data is not None:
groupdata.append(action_data)
groups.append('\n'.join(groupdata))
2017-10-24 09:37:10 +02:00
# pylint: enable=protected-access
options = '\n'.join(groups)
2014-09-05 07:45:47 +02:00
# epilog
if parser.epilog is not None:
options += parser.epilog
2014-09-05 07:45:47 +02:00
_format_block(filename, 'options', options)
2016-04-01 18:18:41 +02:00
def regenerate_cheatsheet():
"""Generate cheatsheet PNGs based on the SVG."""
files = [
('doc/img/cheatsheet-small.png', 300, 185),
('doc/img/cheatsheet-big.png', 3342, 2060),
]
for filename, x, y in files:
subprocess.run(['inkscape', '-e', filename, '-b', 'white',
'-w', str(x), '-h', str(y),
'misc/cheatsheet.svg'], check=True)
2016-04-01 18:18:41 +02:00
2014-09-08 07:44:32 +02:00
def main():
"""Regenerate all documentation."""
utils.change_cwd()
2018-12-10 16:00:31 +01:00
loader.load_components(skip_hooks=True)
2014-10-08 22:24:05 +02:00
print("Generating manpage...")
2014-09-05 07:45:47 +02:00
regenerate_manpage('doc/qutebrowser.1.asciidoc')
2014-10-08 22:24:05 +02:00
print("Generating settings help...")
2014-09-08 12:18:54 +02:00
generate_settings('doc/help/settings.asciidoc')
2014-10-08 22:24:05 +02:00
print("Generating command help...")
2014-09-08 12:18:54 +02:00
generate_commands('doc/help/commands.asciidoc')
2016-04-01 18:18:41 +02:00
if '--cheatsheet' in sys.argv:
print("Regenerating cheatsheet .pngs")
regenerate_cheatsheet()
2014-09-16 20:08:08 +02:00
if '--html' in sys.argv:
asciidoc2html.main()
2014-09-08 07:44:32 +02:00
if __name__ == '__main__':
main()