diff --git a/qutebrowser/app.py b/qutebrowser/app.py index becd548f6..0b7dd2d13 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -581,7 +581,7 @@ class Application(QApplication): self._destroy_crashlogfile() sys.exit(1) - @cmdutils.register(instance='', nargs=0) + @cmdutils.register(instance='', ignore_args=True) def restart(self, shutdown=True, pages=None): """Restart qutebrowser while keeping existing tabs open.""" # We don't use _recover_pages here as it's too forgiving when diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 13173c674..9006c0fbc 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -78,9 +78,7 @@ class CommandDispatcher: if perc is None and count is None: perc = 100 elif perc is None: - perc = int(count) - else: - perc = float(perc) + perc = count perc = qtutils.check_overflow(perc, 'int', fatal=False) frame = self._current_widget().page().currentFrame() m = frame.scrollBarMaximum(orientation) @@ -161,28 +159,35 @@ class CommandDispatcher: @cmdutils.register(instance='mainwindow.tabs.cmd', name='open', split=False) - def openurl(self, urlstr, count=None): + def openurl(self, urlstr, bg=False, tab=False, count=None): """Open a URL in the current/[count]th tab. Args: urlstr: The URL to open, as string. + bg: Whether to open in a background tab. + tab: Whether to open in a tab. count: The tab index to open the URL in, or None. """ - tab = self._tabs.cntwidget(count) try: url = urlutils.fuzzy_url(urlstr) except urlutils.FuzzyUrlError as e: raise cmdexc.CommandError(e) - if tab is None: - if count is None: - # We want to open a URL in the current tab, but none exists - # yet. - self._tabs.tabopen(url) - else: - # Explicit count with a tab that doesn't exist. - return + if tab: + self._tabs.tabopen(url, background=False, explicit=True) + elif bg: + self._tabs.tabopen(url, background=True, explicit=True) else: - tab.openurl(url) + curtab = self._tabs.cntwidget(count) + if curtab is None: + if count is None: + # We want to open a URL in the current tab, but none exists + # yet. + self._tabs.tabopen(url) + else: + # Explicit count with a tab that doesn't exist. + return + else: + curtab.openurl(url) @cmdutils.register(instance='mainwindow.tabs.cmd', name='reload') def reloadpage(self, count=None): @@ -206,29 +211,12 @@ class CommandDispatcher: if tab is not None: tab.stop() - @cmdutils.register(instance='mainwindow.tabs.cmd') - def print_preview(self, count=None): - """Preview printing of the current/[count]th tab. - - Args: - count: The tab index to print, or None. - """ - if not qtutils.check_print_compat(): - # WORKAROUND (remove this when we bump the requirements to 5.3.0) - raise cmdexc.CommandError( - "Printing on Qt < 5.3.0 on Windows is broken, please upgrade!") - tab = self._tabs.cntwidget(count) - if tab is not None: - preview = QPrintPreviewDialog() - preview.setAttribute(Qt.WA_DeleteOnClose) - preview.paintRequested.connect(tab.print) - preview.exec_() - @cmdutils.register(instance='mainwindow.tabs.cmd', name='print') - def printpage(self, count=None): + def printpage(self, preview=False, count=None): """Print the current/[count]th tab. Args: + preview: Whether to preview instead of printing. count: The tab index to print, or None. """ if not qtutils.check_print_compat(): @@ -237,9 +225,15 @@ class CommandDispatcher: "Printing on Qt < 5.3.0 on Windows is broken, please upgrade!") tab = self._tabs.cntwidget(count) if tab is not None: - printdiag = QPrintDialog() - printdiag.setAttribute(Qt.WA_DeleteOnClose) - printdiag.open(lambda: tab.print(printdiag.printer())) + if preview: + diag = QPrintPreviewDialog() + diag.setAttribute(Qt.WA_DeleteOnClose) + diag.paintRequested.connect(tab.print) + diag.exec_() + else: + diag = QPrintDialog() + diag.setAttribute(Qt.WA_DeleteOnClose) + diag.open(lambda: tab.print(printdiag.printer())) @cmdutils.register(instance='mainwindow.tabs.cmd') def back(self, count=1): @@ -262,7 +256,8 @@ class CommandDispatcher: self._current_widget().go_forward() @cmdutils.register(instance='mainwindow.tabs.cmd') - def hint(self, group='all', target='normal', *args): + def hint(self, group=webelem.Group.all, target=hints.Target.normal, + *args : {'nargs': '*'}): """Start hinting. Args: @@ -281,9 +276,9 @@ class CommandDispatcher: - `yank-primary`: Yank the link to the primary selection. - `fill`: Fill the commandline with the command given as argument. - - `cmd-tab`: Fill the commandline with `:open-tab` and the + - `cmd-tab`: Fill the commandline with `:open -t` and the link. - - `cmd-tag-bg`: Fill the commandline with `:open-tab-bg` and + - `cmd-tag-bg`: Fill the commandline with `:open -b` and the link. - `rapid`: Open the link in a new tab and stay in hinting mode. - `download`: Download the link. @@ -305,18 +300,8 @@ class CommandDispatcher: frame = widget.page().mainFrame() if frame is None: raise cmdexc.CommandError("No frame focused!") - try: - group_enum = webelem.Group[group.replace('-', '_')] - except KeyError: - raise cmdexc.CommandError("Unknown hinting group {}!".format( - group)) - try: - target_enum = hints.Target[target.replace('-', '_')] - except KeyError: - raise cmdexc.CommandError("Unknown hinting target {}!".format( - target)) - widget.hintmanager.start(frame, self._tabs.current_url(), group_enum, - target_enum, *args) + widget.hintmanager.start(frame, self._tabs.current_url(), group, + target, *args) @cmdutils.register(instance='mainwindow.tabs.cmd', hide=True) def follow_hint(self): @@ -324,43 +309,31 @@ class CommandDispatcher: self._current_widget().hintmanager.follow_hint() @cmdutils.register(instance='mainwindow.tabs.cmd') - def prev_page(self): + def prev_page(self, tab=False): """Open a "previous" link. This tries to automaticall click on typical "Previous Page" links using some heuristics. + + Args: + tab: Whether to open a new tab. """ - self._prevnext(prev=True, newtab=False) + self._prevnext(prev=True, newtab=tab) @cmdutils.register(instance='mainwindow.tabs.cmd') - def next_page(self): + def next_page(self, tab=False): """Open a "next" link. This tries to automatically click on typical "Next Page" links using some heuristics. + + Args: + tab: Whether to open a new tab. """ - self._prevnext(prev=False, newtab=False) - - @cmdutils.register(instance='mainwindow.tabs.cmd') - def prev_page_tab(self): - """Open a "previous" link in a new tab. - - This tries to automatically click on typical "Previous Page" links - using some heuristics. - """ - self._prevnext(prev=True, newtab=True) - - @cmdutils.register(instance='mainwindow.tabs.cmd') - def next_page_tab(self): - """Open a "next" link in a new tab. - - This tries to automatically click on typical "Previous Page" links - using some heuristics. - """ - self._prevnext(prev=False, newtab=True) + self._prevnext(prev=False, newtab=tab) @cmdutils.register(instance='mainwindow.tabs.cmd', hide=True) - def scroll(self, dx, dy, count=1): + def scroll(self, dx : float, dy : float, count=1): """Scroll the current tab by 'count * dx/dy'. Args: @@ -368,40 +341,30 @@ class CommandDispatcher: dy: How much to scroll in x-direction. count: multiplier """ - dx = int(int(count) * float(dx)) - dy = int(int(count) * float(dy)) + dx *= count + dy *= count cmdutils.check_overflow(dx, 'int') cmdutils.check_overflow(dy, 'int') self._current_widget().page().currentFrame().scroll(dx, dy) @cmdutils.register(instance='mainwindow.tabs.cmd', hide=True) - def scroll_perc_x(self, perc=None, count=None): - """Scroll horizontally to a specific percentage of the page. + def scroll_perc(self, perc : float = None, + horizontal : {'flag': 'x'} = False, count=None): + """Scroll to a specific percentage of the page. The percentage can be given either as argument or as count. If no percentage is given, the page is scrolled to the end. Args: perc: Percentage to scroll. + horizontal: Whether to scroll horizontally. count: Percentage to scroll. """ - self._scroll_percent(perc, count, Qt.Horizontal) + self._scroll_percent(perc, count, + Qt.Horizontal if horizontal else Qt.Vertical) @cmdutils.register(instance='mainwindow.tabs.cmd', hide=True) - def scroll_perc_y(self, perc=None, count=None): - """Scroll vertically to a specific percentage of the page. - - The percentage can be given either as argument or as count. - If no percentage is given, the page is scrolled to the end. - - Args: - perc: Percentage to scroll. - count: Percentage to scroll. - """ - self._scroll_percent(perc, count, Qt.Vertical) - - @cmdutils.register(instance='mainwindow.tabs.cmd', hide=True) - def scroll_page(self, x, y, count=1): + def scroll_page(self, x : int, y : int, count=1): """Scroll the frame page-wise. Args: @@ -411,52 +374,36 @@ class CommandDispatcher: """ frame = self._current_widget().page().currentFrame() size = frame.geometry() - dx = int(count) * float(x) * size.width() - dy = int(count) * float(y) * size.height() + dx = count * x * size.width() + dy = count * y * size.height() cmdutils.check_overflow(dx, 'int') cmdutils.check_overflow(dy, 'int') frame.scroll(dx, dy) @cmdutils.register(instance='mainwindow.tabs.cmd') - def yank(self, sel=False): - """Yank the current URL to the clipboard or primary selection. + def yank(self, title=False, sel=False): + """Yank the current URL/title to the clipboard or primary selection. Args: sel: True to use primary selection, False to use clipboard + title: Whether to yank the title instead of the URL. """ clipboard = QApplication.clipboard() - urlstr = self._tabs.current_url().toString( - QUrl.FullyEncoded | QUrl.RemovePassword) + if title: + s = self._tabs.tabText(self._tabs.currentIndex()) + else: + s = self._tabs.current_url().toString( + QUrl.FullyEncoded | QUrl.RemovePassword) if sel and clipboard.supportsSelection(): mode = QClipboard.Selection target = "primary selection" else: mode = QClipboard.Clipboard target = "clipboard" - log.misc.debug("Yanking to {}: '{}'".format(target, urlstr)) - clipboard.setText(urlstr, mode) + log.misc.debug("Yanking to {}: '{}'".format(target, s)) + clipboard.setText(s, mode) message.info("URL yanked to {}".format(target)) - @cmdutils.register(instance='mainwindow.tabs.cmd') - def yank_title(self, sel=False): - """Yank the current title to the clipboard or primary selection. - - Args: - sel: True to use primary selection, False to use clipboard - """ - clipboard = QApplication.clipboard() - title = self._tabs.tabText(self._tabs.currentIndex()) - mode = QClipboard.Selection if sel else QClipboard.Clipboard - if sel and clipboard.supportsSelection(): - mode = QClipboard.Selection - target = "primary selection" - else: - mode = QClipboard.Clipboard - target = "clipboard" - log.misc.debug("Yanking to {}: '{}'".format(target, title)) - clipboard.setText(title, mode) - message.info("Title yanked to {}".format(target)) - @cmdutils.register(instance='mainwindow.tabs.cmd') def zoom_in(self, count=1): """Increase the zoom level for the current tab. @@ -503,24 +450,6 @@ class CommandDispatcher: continue self._tabs.close_tab(tab) - @cmdutils.register(instance='mainwindow.tabs.cmd', split=False) - def open_tab(self, urlstr): - """Open a new tab with a given url.""" - try: - url = urlutils.fuzzy_url(urlstr) - except urlutils.FuzzyUrlError as e: - raise cmdexc.CommandError(e) - self._tabs.tabopen(url, background=False, explicit=True) - - @cmdutils.register(instance='mainwindow.tabs.cmd', split=False) - def open_tab_bg(self, urlstr): - """Open a new tab in background.""" - try: - url = urlutils.fuzzy_url(urlstr) - except urlutils.FuzzyUrlError as e: - raise cmdexc.CommandError(e) - self._tabs.tabopen(url, background=True, explicit=True) - @cmdutils.register(instance='mainwindow.tabs.cmd') def undo(self): """Re-open a closed tab (optionally skipping [count] closed tabs).""" @@ -559,7 +488,7 @@ class CommandDispatcher: else: raise cmdexc.CommandError("Last tab") - @cmdutils.register(instance='mainwindow.tabs.cmd', nargs=(0, 1)) + @cmdutils.register(instance='mainwindow.tabs.cmd') def paste(self, sel=False, tab=False): """Open a page from the clipboard. @@ -589,16 +518,7 @@ class CommandDispatcher: widget.openurl(url) @cmdutils.register(instance='mainwindow.tabs.cmd') - def paste_tab(self, sel=False): - """Open a page from the clipboard in a new tab. - - Args: - sel: True to use primary selection, False to use clipboard - """ - self.paste(sel, True) - - @cmdutils.register(instance='mainwindow.tabs.cmd') - def tab_focus(self, index=None, count=None): + def tab_focus(self, index : int = None, count=None): """Select the tab given as argument/[count]. Args: @@ -622,7 +542,7 @@ class CommandDispatcher: idx)) @cmdutils.register(instance='mainwindow.tabs.cmd') - def tab_move(self, direction=None, count=None): + def tab_move(self, direction : ('+', '-') = None, count=None): """Move the current tab. Args: @@ -698,26 +618,19 @@ class CommandDispatcher: quickmarks.prompt_save(self._tabs.current_url()) @cmdutils.register(instance='mainwindow.tabs.cmd') - def quickmark_load(self, name): + def quickmark_load(self, name, tab=False, bg=False): """Load a quickmark.""" urlstr = quickmarks.get(name) url = QUrl(urlstr) if not url.isValid(): raise cmdexc.CommandError("Invalid URL {} ({})".format( urlstr, url.errorString())) - self._current_widget().openurl(url) - - @cmdutils.register(instance='mainwindow.tabs.cmd') - def quickmark_load_tab(self, name): - """Load a quickmark in a new tab.""" - url = quickmarks.get(name) - self._tabs.tabopen(url, background=False, explicit=True) - - @cmdutils.register(instance='mainwindow.tabs.cmd') - def quickmark_load_tab_bg(self, name): - """Load a quickmark in a new background tab.""" - url = quickmarks.get(name) - self._tabs.tabopen(url, background=True, explicit=True) + if tab: + self._tabs.tabopen(url, background=False, explicit=True) + elif bg: + self._tabs.tabopen(url, background=True, explicit=True) + else: + self._current_widget().openurl(url) @cmdutils.register(instance='mainwindow.tabs.cmd', name='inspector') def toggle_inspector(self): diff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py new file mode 100644 index 000000000..a31f0988d --- /dev/null +++ b/qutebrowser/commands/argparser.py @@ -0,0 +1,40 @@ +# 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 . + +"""argparse.ArgumentParser subclass to parse qutebrowser commands.""" + +import argparse + + +class ArgumentParserError(Exception): + + """Exception raised when the ArgumentParser signals an error.""" + + +class ArgumentParser(argparse.ArgumentParser): + + def __init__(self): + super().__init__(add_help=False) + + def exit(self, status=0, msg=None): + raise AssertionError('exit called, this should never happen. ' + 'Status: {}, message: {}'.format(status, msg)) + + def error(self, msg): + raise ArgumentParserError(msg) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 4fe0cc9d1..4735bad53 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -23,11 +23,12 @@ Module attributes: cmd_dict: A mapping from command-strings to command objects. """ +import enum import inspect import collections -from qutebrowser.utils import usertypes, qtutils -from qutebrowser.commands import command, cmdexc +from qutebrowser.utils import usertypes, qtutils, log +from qutebrowser.commands import command, cmdexc, argparser cmd_dict = {} @@ -68,23 +69,19 @@ def arg_or_count(arg, count, default=None, countzero=None): The value to use. Raise: - ValueError: If nothing was set or the value couldn't be converted to - an integer. + ValueError: If nothing was set. """ if count is not None and arg is not None: raise ValueError("Both count and argument given!") elif arg is not None: - try: - return int(arg) - except ValueError: - raise ValueError("Invalid number: {}".format(arg)) + return arg elif count is not None: if countzero is not None and count == 0: return countzero else: - return int(count) + return count elif default is not None: - return int(default) + return default else: raise ValueError("Either count or argument have to be set!") @@ -99,7 +96,6 @@ class register: # pylint: disable=invalid-name Attributes: instance: The instance to be used as "self", as a dotted string. name: The name (as string) or names (as list) of the command. - nargs: A (minargs, maxargs) tuple of valid argument counts, or an int. split: Whether to split the arguments. hide: Whether to hide the command or not. completion: Which completion to use for arguments, as a list of @@ -107,11 +103,18 @@ class register: # pylint: disable=invalid-name modes/not_modes: List of modes to use/not use. needs_js: If javascript is needed for this command. debug: Whether this is a debugging command (only shown with --debug). + ignore_args: Whether to ignore the arguments of the function. + + Class attributes: + AnnotationInfo: Named tuple for info from an annotation. """ - def __init__(self, instance=None, name=None, nargs=None, split=True, - hide=False, completion=None, modes=None, not_modes=None, - needs_js=False, debug=False): + AnnotationInfo = collections.namedtuple('AnnotationInfo', + 'kwargs, typ, name, flag') + + def __init__(self, instance=None, name=None, split=True, hide=False, + completion=None, modes=None, not_modes=None, needs_js=False, + debug=False, ignore_args=False): """Save decorator arguments. Gets called on parse-time with the decorator arguments. @@ -125,13 +128,13 @@ class register: # pylint: disable=invalid-name self.name = name self.split = split self.hide = hide - self.nargs = nargs self.instance = instance self.completion = completion self.modes = modes self.not_modes = not_modes self.needs_js = needs_js self.debug = debug + self.ignore_args = ignore_args if modes is not None: for m in modes: if not isinstance(m, usertypes.KeyMode): @@ -155,74 +158,156 @@ class register: # pylint: disable=invalid-name Return: The original function (unmodified). """ - names = [] - if self.name is None: - name = func.__name__.lower().replace('_', '-') - else: - name = self.name - if isinstance(name, str): - mainname = name - names.append(name) - else: - mainname = name[0] - names += name - if mainname in cmd_dict: + names = self._get_names(func) + log.commands.vdebug("Registering command {}".format(names[0])) + if any(name in cmd_dict for name in names): raise ValueError("{} is already registered!".format(name)) - argspec = inspect.getfullargspec(func) - if 'self' in argspec.args and self.instance is None: - raise ValueError("{} is a class method, but instance was not " - "given!".format(mainname)) - count, nargs = self._get_nargs_count(argspec) - if func.__doc__ is not None: - desc = func.__doc__.splitlines()[0].strip() - else: - desc = "" + has_count, desc, parser = self._inspect_func(func) cmd = command.Command( - name=mainname, split=self.split, hide=self.hide, nargs=nargs, - count=count, desc=desc, instance=self.instance, handler=func, + name=names[0], split=self.split, hide=self.hide, count=has_count, + desc=desc, instance=self.instance, handler=func, completion=self.completion, modes=self.modes, - not_modes=self.not_modes, needs_js=self.needs_js, debug=self.debug) + not_modes=self.not_modes, needs_js=self.needs_js, debug=self.debug, + parser=parser) for name in names: cmd_dict[name] = cmd return func - def _get_nargs_count(self, spec): - """Get the number of command-arguments and count-support for a func. + def _get_names(self, func): + """Get the name(s) which should be used for the current command. + + If the name hasn't been overridden explicitely, the function name is + transformed. + + If it has been set, it can either be a string which is + used directly, or an iterable. Args: - spec: A FullArgSpec as returned by inspect. + func: The function to get the name for. Return: - A (count, (minargs, maxargs)) tuple, with maxargs=None if there are - infinite args. count is True if the function supports count, else - False. - - Mapping from old nargs format to (minargs, maxargs): - ? (0, 1) - N (N, N) - + (1, None) - * (0, None) + A list of names, with the main name being the first item. """ - count = 'count' in spec.args - # we assume count always has a default (and it should!) - if self.nargs is not None: - # If nargs is overriden, use that. - if isinstance(self.nargs, collections.Iterable): - # Iterable (min, max) - # pylint: disable=unpacking-non-sequence - minargs, maxargs = self.nargs - else: - # Single int - minargs, maxargs = self.nargs, self.nargs + if self.name is None: + return [func.__name__.lower().replace('_', '-')] + elif isinstance(self.name, str): + return [self.name] else: - defaultcount = (len(spec.defaults) if spec.defaults is not None - else 0) - argcount = len(spec.args) - if 'self' in spec.args: - argcount -= 1 - minargs = argcount - defaultcount - if spec.varargs is not None: - maxargs = None + return self.name + + def _inspect_func(self, func): + """Inspect a function to get useful informations from it. + + Args: + func: The function to look at. + + Return: + A (has_count, desc, parser) tuple. + has_count: Whether the command supports a count. + desc: The description of the command. + parser: The ArgumentParser to use when parsing the commandline. + """ + signature = inspect.signature(func) + if 'self' in signature.parameters and self.instance is None: + raise ValueError("{} is a class method, but instance was not " + "given!".format(mainname)) + has_count = 'count' in signature.parameters + parser = argparser.ArgumentParser() + if func.__doc__ is not None: + desc = func.__doc__.splitlines()[0].strip() + else: + desc = "" + if not self.ignore_args: + for param in signature.parameters.values(): + if param.name in ('self', 'count'): + continue + args = [] + kwargs = {} + annotation_info = self._parse_annotation(param) + if annotation_info.typ is not None: + typ = annotation_info.typ + else: + typ = self._infer_type(param) + kwargs.update(self._type_to_argparse(typ)) + kwargs.update(annotation_info.kwargs) + if (param.kind == inspect.Parameter.VAR_POSITIONAL and + 'nargs' not in kwargs): # annotation_info overrides it + kwargs['nargs'] = '*' + is_flag = typ == bool + args += self._get_argparse_args(param, annotation_info, + is_flag) + log.commands.vdebug('Adding argument {} of type {} -> ' + 'args {}, kwargs {}'.format( + param.name, typ, args, kwargs)) + parser.add_argument(*args, **kwargs) + return has_count, desc, parser + + def _get_argparse_args(self, param, annotation_info, is_flag): + """Get a list of positional argparse arguments. + + Args: + param: The inspect.Parameter instance for the current parameter. + annotation_info: An AnnotationInfo tuple for the parameter. + is_flag: Whether the option is a flag or not. + """ + args = [] + name = annotation_info.name or param.name + shortname = annotation_info.flag or param.name[0] + if is_flag: + args.append('--{}'.format(name)) + args.append('-{}'.format(shortname)) + else: + args.append(name) + return args + + def _parse_annotation(self, param): + """Get argparse arguments and type from a parameter annotation. + + Args: + param: A inspect.Parameter instance. + + Return: + An AnnotationInfo namedtuple. + kwargs: A dict of keyword args to add to the + argparse.ArgumentParser.add_argument call. + typ: The type to use for this argument. + flag: The short name/flag if overridden. + name: The long name if overridden. + """ + info = {'kwargs': {}, 'typ': None, 'flag': None, 'name': None} + log.commands.vdebug("Parsing annotation {}".format(param.annotation)) + if param.annotation is not inspect.Parameter.empty: + if isinstance(param.annotation, dict): + for field in ('type', 'flag', 'name'): + if field in param.annotation: + info[field] = param.annotation[field] + del param.annotation[field] + info['kwargs'] = param.annotation else: - maxargs = argcount - int(count) # -1 if count is defined - return (count, (minargs, maxargs)) + info['typ'] = param.annotation + return self.AnnotationInfo(**info) + + def _infer_type(self, param): + """Get the type of an argument from its default value.""" + if param.default is None or param.default is inspect.Parameter.empty: + return None + else: + return type(param.default) + + def _type_to_argparse(self, typ): + """Get argparse keyword arguments based on a type.""" + kwargs = {} + try: + is_enum = issubclass(typ, enum.Enum) + except TypeError: + is_enum = False + if isinstance(typ, tuple): + kwargs['choices'] = typ + elif is_enum: + kwargs['choices'] = [e.name.replace('_', '-') for e in typ] + elif typ is bool: + kwargs['action'] = 'store_true' + elif typ is not None: + kwargs['type'] = typ + + return kwargs diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 4d774198f..0780c3519 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -19,11 +19,11 @@ """Contains the Command class, a skeleton for a command.""" -from PyQt5.QtCore import QCoreApplication, QUrl +from PyQt5.QtCore import QCoreApplication from PyQt5.QtWebKit import QWebSettings -from qutebrowser.commands import cmdexc -from qutebrowser.utils import log, utils +from qutebrowser.commands import cmdexc, argparser +from qutebrowser.utils import log, utils, message class Command: @@ -34,7 +34,6 @@ class Command: name: The main name of the command. split: Whether to split the arguments. hide: Whether to hide the arguments or not. - nargs: A (minargs, maxargs) tuple, maxargs = None if there's no limit. count: Whether the command supports a count, or not. desc: The description of the command. instance: How to get to the "self" argument of the handler. @@ -43,20 +42,20 @@ class Command: completion: Completions to use for arguments, as a list of strings. needs_js: Whether the command needs javascript enabled debug: Whether this is a debugging command (only shown with --debug). + parser: The ArgumentParser to use to parse this command. """ # TODO: # we should probably have some kind of typing / argument casting for args # this might be combined with help texts or so as well - def __init__(self, name, split, hide, nargs, count, desc, instance, - handler, completion, modes, not_modes, needs_js, debug): + def __init__(self, name, split, hide, count, desc, instance, handler, + completion, modes, not_modes, needs_js, debug, parser): # I really don't know how to solve this in a better way, I tried. # pylint: disable=too-many-arguments self.name = name self.split = split self.hide = hide - self.nargs = nargs self.count = count self.desc = desc self.instance = instance @@ -66,15 +65,12 @@ class Command: self.not_modes = not_modes self.needs_js = needs_js self.debug = debug + self.parser = parser - def check(self, args): - """Check if the argument count is valid and the command is permitted. - - Args: - args: The supplied arguments + def _check_prerequisites(self): + """Check if the command is permitted to run currently. Raise: - ArgumentCountError if the argument count is wrong. PrerequisitesError if the command can't be called currently. """ # We don't use modeman.instance() here to avoid a circular import @@ -94,20 +90,6 @@ class Command: QWebSettings.JavascriptEnabled): raise cmdexc.PrerequisitesError( "{}: This command needs javascript enabled.".format(self.name)) - if self.nargs[1] is None and self.nargs[0] <= len(args): - pass - elif self.nargs[0] <= len(args) <= self.nargs[1]: - pass - else: - if self.nargs[0] == self.nargs[1]: - argcnt = str(self.nargs[0]) - elif self.nargs[1] is None: - argcnt = '{}-inf'.format(self.nargs[0]) - else: - argcnt = '{}-{}'.format(self.nargs[0], self.nargs[1]) - raise cmdexc.ArgumentCountError( - "{}: {} args expected, but got {}".format(self.name, argcnt, - len(args))) def run(self, args=None, count=None): """Run the command. @@ -120,23 +102,29 @@ class Command: """ dbgout = ["command called:", self.name] if args: - dbgout += args + dbgout.append(str(args)) if count is not None: dbgout.append("(count={})".format(count)) log.commands.debug(' '.join(dbgout)) + posargs = [] kwargs = {} app = QCoreApplication.instance() - # Replace variables (currently only {url}) - new_args = [] - for arg in args: - if arg == '{url}': - urlstr = app.mainwindow.tabs.current_url().toString( - QUrl.FullyEncoded | QUrl.RemovePassword) - new_args.append(urlstr) + try: + namespace = self.parser.parse_args(args) + except argparser.ArgumentParserError as e: + message.error(str(e)) + return + + for name, arg in vars(namespace).items(): + if isinstance(arg, list): + # If we got a list, we assume that's our *args, so we don't add + # it to kwargs. + # FIXME: This approach is rather naive, but for now it works. + posargs += arg else: - new_args.append(arg) + kwargs[name] = arg if self.instance is not None: # Add the 'self' parameter. @@ -144,9 +132,12 @@ class Command: obj = app else: obj = utils.dotted_getattr(app, self.instance) - new_args.insert(0, obj) + posargs.insert(0, obj) if count is not None and self.count: kwargs = {'count': count} - self.handler(*new_args, **kwargs) + self._check_prerequisites() + log.commands.debug('posargs: {}'.format(posargs)) + log.commands.debug('kwargs: {}'.format(kwargs)) + self.handler(*posargs, **kwargs) diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 95d00e4b7..7ad83ed9d 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -19,7 +19,7 @@ """Module containing command managers (SearchRunner and CommandRunner).""" -from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject +from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QCoreApplication, QUrl from PyQt5.QtWebKitWidgets import QWebPage from qutebrowser.config import config @@ -198,14 +198,18 @@ class CommandRunner: parts = text.strip().split(maxsplit=1) if not parts: raise cmdexc.NoSuchCommandError("No command given") - cmdstr = parts[0] + elif len(parts) > 1: + cmdstr, argstr = parts + else: + cmdstr = parts[0] + argstr = None if aliases: new_cmd = self._get_alias(text, alias_no_args) if new_cmd is not None: log.commands.debug("Re-parsing with '{}'.".format(new_cmd)) return self.parse(new_cmd, aliases=False) try: - cmd = cmdutils.cmd_dict[cmdstr] + self._cmd = cmdutils.cmd_dict[cmdstr] except KeyError: if fallback: parts = text.split(' ') @@ -215,36 +219,38 @@ class CommandRunner: else: raise cmdexc.NoSuchCommandError( '{}: no such command'.format(cmdstr)) - if len(parts) == 1: - args = [] - elif cmd.split: - args = utils.safe_shlex_split(parts[1]) + if argstr is None: + self._args = [] + elif self._cmd.split: + self._args = utils.safe_shlex_split(argstr) else: - args = parts[1].split(maxsplit=cmd.nargs[0] - 1) - self._cmd = cmd - self._args = args - retargs = args[:] + # If split=False, we still want to split the flags, but not + # everything after that. + # We first split the arg string and check the index of the first + # non-flag args, then we re-split again properly. + # example: + # + # input: "--foo -v bar baz" + # first split: ['--foo', '-v', 'bar', 'baz'] + # 0 1 2 3 + # second split: ['--foo', '-v', 'bar baz'] + # (maxsplit=2) + split_args = argstr.split() + for i, arg in enumerate(split_args): + if not arg.startswith('-'): + self._args = argstr.split(maxsplit=i) + break + else: + # If there are only flags, we got it right on the first try + # already. + self._args = split_args + retargs = self._args[:] if text.endswith(' '): retargs.append('') return [cmdstr] + retargs - def _check(self): - """Check if the argument count for the command is correct.""" - self._cmd.check(self._args) - - def _run(self, count=None): - """Run a command with an optional count. - - Args: - count: Count to pass to the command. - """ - if count is not None: - self._cmd.run(self._args, count=count) - else: - self._cmd.run(self._args) - def run(self, text, count=None): - """Parse a command from a line of text. + """Parse a command from a line of text and run it. Args: text: The text to parse. @@ -255,8 +261,15 @@ class CommandRunner: self.run(sub, count) return self.parse(text) - self._check() - self._run(count=count) + app = QCoreApplication.instance() + cur_url = app.mainwindow.tabs.current_url().toString( + QUrl.FullyEncoded | QUrl.RemovePassword) + self._args = [cur_url if e == '{url}' else e for e in self._args] + if count is not None: + self._cmd.run(self._args, count=count) + else: + self._cmd.run(self._args) + @pyqtSlot(str, int) def run_safely(self, text, count=None): diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 986afd62f..3d0f6516e 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -336,7 +336,7 @@ class ConfigManager(QObject): @cmdutils.register(name='set', instance='config', completion=[Completion.section, Completion.option, Completion.value]) - def set_wrapper(self, sectname, optname, value): + def set_command(self, sectname, optname, value, temp=False): """Set an option. // @@ -347,36 +347,15 @@ class ConfigManager(QObject): sectname: The section where the option is in. optname: The name of the option. value: The value to set. + temp: Set value temporarely. """ try: - self.set('conf', sectname, optname, value) + self.set('temp' if temp else 'conf', sectname, optname, value) except (NoOptionError, NoSectionError, configtypes.ValidationError, ValueError) as e: raise cmdexc.CommandError("set: {} - {}".format( e.__class__.__name__, e)) - @cmdutils.register(name='set-temp', instance='config', - completion=[Completion.section, Completion.option, - Completion.value]) - def set_temp_wrapper(self, sectname, optname, value): - """Set a temporary option. - - // - - Wrapper for self.set() to output exceptions in the status bar. - - Args: - sectname: The section where the option is in. - optname: The name of the option. - value: The value to set. - """ - try: - self.set('temp', sectname, optname, value) - except (NoOptionError, NoSectionError, - configtypes.ValidationError) as e: - raise cmdexc.CommandError("set: {} - {}".format( - e.__class__.__name__, e)) - def set(self, layer, sectname, optname, value): """Set an option. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 2c79bd209..7afff556c 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -585,11 +585,11 @@ DATA = collections.OrderedDict([ typ.KeyBindingName(), typ.KeyBinding(), ('o', 'set-cmd-text ":open "'), ('go', 'set-cmd-text :open {url}'), - ('O', 'set-cmd-text ":open-tab "'), - ('gO', 'set-cmd-text :open-tab {url}'), - ('xo', 'set-cmd-text ":open-tab-bg "'), - ('xO', 'set-cmd-text :open-tab-bg {url}'), - ('ga', 'open-tab about:blank'), + ('O', 'set-cmd-text ":open -t "'), + ('gO', 'set-cmd-text :open -t {url}'), + ('xo', 'set-cmd-text ":open -b "'), + ('xO', 'set-cmd-text :open -b {url}'), + ('ga', 'open -t about:blank'), ('d', 'tab-close'), ('co', 'tab-only'), ('T', 'tab-focus'), @@ -608,8 +608,8 @@ DATA = collections.OrderedDict([ (';I', 'hint images tab'), ('.i', 'hint images tab-bg'), (';o', 'hint links fill :open {hint-url}'), - (';O', 'hint links fill :open-tab {hint-url}'), - ('.o', 'hint links fill :open-tab-bg {hint-url}'), + (';O', 'hint links fill :open -t {hint-url}'), + ('.o', 'hint links fill :open -b {hint-url}'), (';y', 'hint links yank'), (';Y', 'hint links yank-primary'), (';r', 'hint links rapid'), @@ -619,33 +619,33 @@ DATA = collections.OrderedDict([ ('k', 'scroll 0 -50'), ('l', 'scroll 50 0'), ('u', 'undo'), - ('gg', 'scroll-perc-y 0'), - ('G', 'scroll-perc-y'), + ('gg', 'scroll-perc 0'), + ('G', 'scroll-perc'), ('n', 'search-next'), ('N', 'search-prev'), ('i', 'enter-mode insert'), ('yy', 'yank'), - ('yY', 'yank sel'), - ('yt', 'yank-title'), - ('yT', 'yank-title sel'), + ('yY', 'yank -s'), + ('yt', 'yank -t'), + ('yT', 'yank -ts'), ('pp', 'paste'), - ('pP', 'paste sel'), - ('Pp', 'paste-tab'), - ('PP', 'paste-tab sel'), + ('pP', 'paste -s'), + ('Pp', 'paste -t'), + ('PP', 'paste -ts'), ('m', 'quickmark-save'), ('b', 'set-cmd-text ":quickmark-load "'), - ('B', 'set-cmd-text ":quickmark-load-tab "'), + ('B', 'set-cmd-text ":quickmark-load -t "'), ('sf', 'save'), ('ss', 'set-cmd-text ":set "'), - ('sl', 'set-cmd-text ":set-temp "'), + ('sl', 'set-cmd-text ":set -t"'), ('sk', 'set-cmd-text ":set keybind "'), ('-', 'zoom-out'), ('+', 'zoom-in'), ('=', 'zoom'), ('[[', 'prev-page'), (']]', 'next-page'), - ('{{', 'prev-page-tab'), - ('}}', 'next-page-tab'), + ('{{', 'prev-page -t'), + ('}}', 'next-page -t'), ('wi', 'inspector'), ('gd', 'download-page'), ('ad', 'cancel-download'), @@ -654,7 +654,7 @@ DATA = collections.OrderedDict([ ('', 'quit'), ('', 'undo'), ('', 'tab-close'), - ('', 'open-tab about:blank'), + ('', 'open -t about:blank'), ('', 'scroll-page 0 1'), ('', 'scroll-page 0 -1'), ('', 'scroll-page 0 0.5'), diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py index d00473437..aa7641143 100644 --- a/qutebrowser/utils/debug.py +++ b/qutebrowser/utils/debug.py @@ -32,7 +32,7 @@ from qutebrowser.config import config, style @cmdutils.register(debug=True) -def debug_crash(typ='exception'): +def debug_crash(typ : ('exception', 'segfault') = 'exception'): """Crash for debugging purposes. Args: diff --git a/qutebrowser/utils/utilcmds.py b/qutebrowser/utils/utilcmds.py index 0bb31769c..82ef05117 100644 --- a/qutebrowser/utils/utilcmds.py +++ b/qutebrowser/utils/utilcmds.py @@ -35,15 +35,14 @@ def init(): _commandrunner = runners.CommandRunner() -@cmdutils.register(nargs=(2, None)) -def later(ms, *command): +@cmdutils.register() +def later(ms : int, *command : {'nargs': '+'}): """Execute a command after some time. Args: ms: How many milliseconds to wait. command: The command/args to run. """ - ms = int(ms) timer = usertypes.Timer(name='later') timer.setSingleShot(True) if ms < 0: diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index 32ad27374..125acd2bd 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -141,7 +141,7 @@ class MainWindow(QWidget): if rect.isValid(): self.completion.setGeometry(rect) - @cmdutils.register(instance='mainwindow', name=['quit', 'q'], nargs=0) + @cmdutils.register(instance='mainwindow', name=['quit', 'q']) def close(self): """Quit qutebrowser. diff --git a/qutebrowser/widgets/webview.py b/qutebrowser/widgets/webview.py index 34b8f3016..b94d74759 100644 --- a/qutebrowser/widgets/webview.py +++ b/qutebrowser/widgets/webview.py @@ -221,20 +221,20 @@ class WebView(QWebView): e: The QMouseEvent. """ if self._force_open_target is not None: - self.open_target = self._force_open_target + self._open_target = self._force_open_target self._force_open_target = None log.mouse.debug("Setting force target: {}".format( - self.open_target)) + self._open_target)) elif (e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier): if config.get('tabs', 'background-tabs'): - self.open_target = usertypes.ClickTarget.tab_bg + self._open_target = usertypes.ClickTarget.tab_bg else: - self.open_target = usertypes.ClickTarget.tab + self._open_target = usertypes.ClickTarget.tab log.mouse.debug("Middle click, setting target: {}".format( - self.open_target)) + self._open_target)) else: - self.open_target = usertypes.ClickTarget.normal + self._open_target = usertypes.ClickTarget.normal log.mouse.debug("Normal click, setting normal target") def shutdown(self):