diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 7a3734fb0..dbf8bbb90 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -26,7 +26,7 @@ from PyQt5.QtCore import QCoreApplication from PyQt5.QtWebKit import QWebSettings from qutebrowser.commands import cmdexc, argparser -from qutebrowser.utils import log, utils, message, debug, usertypes +from qutebrowser.utils import log, utils, message, debug, usertypes, docutils class Command: @@ -74,7 +74,7 @@ class Command: self.debug = is_debug self.ignore_args = ignore_args self.handler = handler - self.docparser = utils.DocstringParser(handler) + self.docparser = docutils.DocstringParser(handler) self.parser = argparser.ArgumentParser( name, description=self.docparser.short_desc, epilog=self.docparser.long_desc) diff --git a/qutebrowser/network/qutescheme.py b/qutebrowser/network/qutescheme.py index e3297aefc..1c7f32c76 100644 --- a/qutebrowser/network/qutescheme.py +++ b/qutebrowser/network/qutescheme.py @@ -31,7 +31,7 @@ from PyQt5.QtNetwork import QNetworkReply import qutebrowser from qutebrowser.network import schemehandler -from qutebrowser.utils import version, utils, jinja, log, message +from qutebrowser.utils import version, utils, jinja, log, message, docutils pyeval_output = ":pyeval was never called" @@ -139,7 +139,7 @@ def qute_help(request): urlpath = 'index.html' else: urlpath = urlpath.lstrip('/') - if not utils.docs_up_to_date(urlpath): + if not docutils.docs_up_to_date(urlpath): message.error("Your documentation is outdated! Please re-run scripts/" "asciidoc2html.py.") path = 'html/doc/{}'.format(urlpath) diff --git a/qutebrowser/utils/docutils.py b/qutebrowser/utils/docutils.py new file mode 100644 index 000000000..946e47394 --- /dev/null +++ b/qutebrowser/utils/docutils.py @@ -0,0 +1,148 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2014 Florian Bruhin (The Compiler) +# +# 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 . + +"""Utilities used for the documentation and built-in help.""" + +import re +import sys +import inspect +import os.path +import collections + +import qutebrowser +from qutebrowser.utils import usertypes + + +def is_git_repo(): + """Check if we're running from a git repository.""" + gitfolder = os.path.join(qutebrowser.basedir, os.path.pardir, '.git') + return os.path.isdir(gitfolder) + + +def docs_up_to_date(path): + """Check if the generated html documentation is up to date. + + Args: + path: The path of the document to check. + + Return: + True if they are up to date or we couldn't check. + False if they are outdated. + """ + if hasattr(sys, 'frozen') or not is_git_repo(): + return True + html_path = os.path.join(qutebrowser.basedir, 'html', 'doc', path) + filename = os.path.splitext(path)[0] + asciidoc_path = os.path.join(qutebrowser.basedir, os.path.pardir, + 'doc', 'help', filename + '.asciidoc') + try: + html_time = os.path.getmtime(html_path) + asciidoc_time = os.path.getmtime(asciidoc_path) + except FileNotFoundError: + return True + return asciidoc_time <= html_time + + +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).replace(', or None', '') + 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) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index b142f96f0..387c53d90 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -21,11 +21,9 @@ import os import io -import re import sys import enum import shlex -import inspect import os.path import urllib.request import urllib.parse @@ -38,7 +36,7 @@ from PyQt5.QtGui import QKeySequence, QColor import pkg_resources import qutebrowser -from qutebrowser.utils import qtutils, log, usertypes +from qutebrowser.utils import qtutils, log def elide(text, length): @@ -580,122 +578,3 @@ def is_enum(obj): return issubclass(obj, enum.Enum) except TypeError: return False - - -def is_git_repo(): - """Check if we're running from a git repository.""" - gitfolder = os.path.join(qutebrowser.basedir, os.path.pardir, '.git') - return os.path.isdir(gitfolder) - - -def docs_up_to_date(path): - """Check if the generated html documentation is up to date. - - Args: - path: The path of the document to check. - - Return: - True if they are up to date or we couldn't check. - False if they are outdated. - """ - if hasattr(sys, 'frozen') or not is_git_repo(): - return True - html_path = os.path.join(qutebrowser.basedir, 'html', 'doc', path) - filename = os.path.splitext(path)[0] - asciidoc_path = os.path.join(qutebrowser.basedir, os.path.pardir, - 'doc', 'help', filename + '.asciidoc') - try: - html_time = os.path.getmtime(html_path) - asciidoc_time = os.path.getmtime(asciidoc_path) - except FileNotFoundError: - return True - return asciidoc_time <= html_time - - -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).replace(', or None', '') - 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) diff --git a/scripts/src2asciidoc.py b/scripts/src2asciidoc.py index 8e6638b42..7408896e0 100755 --- a/scripts/src2asciidoc.py +++ b/scripts/src2asciidoc.py @@ -39,7 +39,7 @@ from scripts import asciidoc2html, utils from qutebrowser import qutebrowser from qutebrowser.commands import cmdutils from qutebrowser.config import configdata -from qutebrowser.utils import utils as quteutils +from qutebrowser.utils import docutils class UsageFormatter(argparse.HelpFormatter): @@ -151,7 +151,7 @@ def _get_command_doc(name, cmd): if syntax != name: output.append('Syntax: +:{}+'.format(syntax)) output.append("") - parser = quteutils.DocstringParser(cmd.handler) + parser = docutils.DocstringParser(cmd.handler) output.append(parser.short_desc) if parser.long_desc: output.append("")