Use annotation instead of special argument names.

Explicit is better than implicit.

Fixes #161.
This commit is contained in:
Florian Bruhin 2014-10-09 06:33:24 +02:00
parent 6b9af36993
commit 0e771db7f1
9 changed files with 91 additions and 56 deletions

View File

@ -380,9 +380,6 @@ def foo():
The commands arguments are automatically deduced by inspecting your function. The commands arguments are automatically deduced by inspecting your function.
If your function has a `count` argument with a default, the command will
support a count which will be passed in the argument.
If the function is a method of a class, the `@cmdutils.register` decorator If the function is a method of a class, the `@cmdutils.register` decorator
needs to have an `instance=...` parameter which points to the (single/main) needs to have an `instance=...` parameter which points to the (single/main)
instance of the class. instance of the class.

View File

@ -147,7 +147,8 @@ class CommandDispatcher:
else: else:
return None return None
def _scroll_percent(self, perc=None, count=None, orientation=None): def _scroll_percent(self, perc=None, count: {'special': 'count'}=None,
orientation=None):
"""Inner logic for scroll_percent_(x|y). """Inner logic for scroll_percent_(x|y).
Args: Args:
@ -246,7 +247,8 @@ class CommandDispatcher:
return None return None
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_close(self, left=False, right=False, opposite=False, count=None): def tab_close(self, left=False, right=False, opposite=False,
count: {'special': 'count'}=None):
"""Close the current/[count]th tab. """Close the current/[count]th tab.
Args: Args:
@ -273,7 +275,8 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', name='open', @cmdutils.register(instance='command-dispatcher', name='open',
split=False, scope='window') split=False, scope='window')
def openurl(self, url, bg=False, tab=False, window=False, count=None): def openurl(self, url, bg=False, tab=False, window=False,
count: {'special': 'count'}=None):
"""Open a URL in the current/[count]th tab. """Open a URL in the current/[count]th tab.
Args: Args:
@ -304,7 +307,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', name='reload', @cmdutils.register(instance='command-dispatcher', name='reload',
scope='window') scope='window')
def reloadpage(self, count=None): def reloadpage(self, count: {'special': 'count'}=None):
"""Reload the current/[count]th tab. """Reload the current/[count]th tab.
Args: Args:
@ -315,7 +318,7 @@ class CommandDispatcher:
tab.reload() tab.reload()
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def stop(self, count=None): def stop(self, count: {'special': 'count'}=None):
"""Stop loading in the current/[count]th tab. """Stop loading in the current/[count]th tab.
Args: Args:
@ -327,7 +330,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', name='print', @cmdutils.register(instance='command-dispatcher', name='print',
scope='window') scope='window')
def printpage(self, preview=False, count=None): def printpage(self, preview=False, count: {'special': 'count'}=None):
"""Print the current/[count]th tab. """Print the current/[count]th tab.
Args: Args:
@ -389,7 +392,8 @@ class CommandDispatcher:
widget.back() widget.back()
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def back(self, tab=False, bg=False, window=False, count=1): def back(self, tab=False, bg=False, window=False,
count: {'special': 'count'}=1):
"""Go back in the history of the current tab. """Go back in the history of the current tab.
Args: Args:
@ -401,7 +405,8 @@ class CommandDispatcher:
self._back_forward(tab, bg, window, count, forward=False) self._back_forward(tab, bg, window, count, forward=False)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def forward(self, tab=False, bg=False, window=False, count=1): def forward(self, tab=False, bg=False, window=False,
count: {'special': 'count'}=1):
"""Go forward in the history of the current tab. """Go forward in the history of the current tab.
Args: Args:
@ -509,7 +514,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', hide=True, @cmdutils.register(instance='command-dispatcher', hide=True,
scope='window') scope='window')
def scroll(self, dx: float, dy: float, count=1): def scroll(self, dx: float, dy: float, count: {'special': 'count'}=1):
"""Scroll the current tab by 'count * dx/dy'. """Scroll the current tab by 'count * dx/dy'.
Args: Args:
@ -526,7 +531,8 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', hide=True, @cmdutils.register(instance='command-dispatcher', hide=True,
scope='window') scope='window')
def scroll_perc(self, perc: float=None, def scroll_perc(self, perc: float=None,
horizontal: {'flag': 'x'}=False, count=None): horizontal: {'flag': 'x'}=False,
count: {'special': 'count'}=None):
"""Scroll to a specific percentage of the page. """Scroll to a specific percentage of the page.
The percentage can be given either as argument or as count. The percentage can be given either as argument or as count.
@ -542,7 +548,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', hide=True, @cmdutils.register(instance='command-dispatcher', hide=True,
scope='window') scope='window')
def scroll_page(self, x: float, y: float, count=1): def scroll_page(self, x: float, y: float, count: {'special': 'count'}=1):
"""Scroll the frame page-wise. """Scroll the frame page-wise.
Args: Args:
@ -584,7 +590,7 @@ class CommandDispatcher:
message.info(self._win_id, "{} yanked to {}".format(what, target)) message.info(self._win_id, "{} yanked to {}".format(what, target))
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def zoom_in(self, count=1): def zoom_in(self, count: {'special': 'count'}=1):
"""Increase the zoom level for the current tab. """Increase the zoom level for the current tab.
Args: Args:
@ -594,7 +600,7 @@ class CommandDispatcher:
tab.zoom(count) tab.zoom(count)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def zoom_out(self, count=1): def zoom_out(self, count: {'special': 'count'}=1):
"""Decrease the zoom level for the current tab. """Decrease the zoom level for the current tab.
Args: Args:
@ -604,7 +610,7 @@ class CommandDispatcher:
tab.zoom(-count) tab.zoom(-count)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def zoom(self, zoom=None, count=None): def zoom(self, zoom=None, count: {'special': 'count'}=None):
"""Set the zoom level for the current tab. """Set the zoom level for the current tab.
The zoom can be given as argument or as [count]. If neither of both is The zoom can be given as argument or as [count]. If neither of both is
@ -650,7 +656,7 @@ class CommandDispatcher:
raise cmdexc.CommandError("Nothing to undo!") raise cmdexc.CommandError("Nothing to undo!")
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_prev(self, count=1): def tab_prev(self, count: {'special': 'count'}=1):
"""Switch to the previous tab, or switch [count] tabs back. """Switch to the previous tab, or switch [count] tabs back.
Args: Args:
@ -665,7 +671,7 @@ class CommandDispatcher:
raise cmdexc.CommandError("First tab") raise cmdexc.CommandError("First tab")
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_next(self, count=1): def tab_next(self, count: {'special': 'count'}=1):
"""Switch to the next tab, or switch [count] tabs forward. """Switch to the next tab, or switch [count] tabs forward.
Args: Args:
@ -707,7 +713,8 @@ class CommandDispatcher:
self._open(url, tab, bg, window) self._open(url, tab, bg, window)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_focus(self, index: (int, 'last')=None, count=None): def tab_focus(self, index: (int, 'last')=None,
count: {'special': 'count'}=None):
"""Select the tab given as argument/[count]. """Select the tab given as argument/[count].
Args: Args:
@ -731,7 +738,8 @@ class CommandDispatcher:
idx)) idx))
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_move(self, direction: ('+', '-')=None, count=None): def tab_move(self, direction: ('+', '-')=None,
count: {'special': 'count'}=None):
"""Move the current tab. """Move the current tab.
Args: Args:

View File

@ -359,7 +359,7 @@ class DownloadManager(QAbstractListModel):
self.fetch(reply) self.fetch(reply)
@cmdutils.register(instance='download-manager') @cmdutils.register(instance='download-manager')
def cancel_download(self, count=1): def cancel_download(self, count: {'special': 'count'}=1):
"""Cancel the first/[count]th download. """Cancel the first/[count]th download.
Args: Args:

View File

@ -74,7 +74,7 @@ def prompt_save(win_id, url):
@cmdutils.register() @cmdutils.register()
def quickmark_add(win_id, url, name): def quickmark_add(win_id: {'special': 'win_id'}, url, name):
"""Add a new quickmark. """Add a new quickmark.
Args: Args:

View File

@ -42,12 +42,14 @@ class Command:
completion: Completions to use for arguments, as a list of strings. completion: Completions to use for arguments, as a list of strings.
debug: Whether this is a debugging command (only shown with --debug). debug: Whether this is a debugging command (only shown with --debug).
parser: The ArgumentParser to use to parse this command. parser: The ArgumentParser to use to parse this command.
special_params: A SpecialParams namedtuple with the names of the
special parameters, or None.
_type_conv: A mapping of conversion functions for arguments. _type_conv: A mapping of conversion functions for arguments.
_name_conv: A mapping of argument names to parameter names. _name_conv: A mapping of argument names to parameter names.
_needs_js: Whether the command needs javascript enabled _needs_js: Whether the command needs javascript enabled
_modes: The modes the command can be executed in. _modes: The modes the command can be executed in.
_not_modes: The modes the command can not be executed in. _not_modes: The modes the command can not be executed in.
_count: Whether the command supports a count, or not. _count: The count set for the command.
_instance: The object to bind 'self' to. _instance: The object to bind 'self' to.
_scope: The scope to get _instance for in the object registry. _scope: The scope to get _instance for in the object registry.
@ -57,8 +59,12 @@ class Command:
""" """
AnnotationInfo = collections.namedtuple('AnnotationInfo', AnnotationInfo = collections.namedtuple('AnnotationInfo',
['kwargs', 'typ', 'name', 'flag']) ['kwargs', 'typ', 'name', 'flag',
'special'])
ParamType = usertypes.enum('ParamType', ['flag', 'positional']) ParamType = usertypes.enum('ParamType', ['flag', 'positional'])
SpecialParams = collections.namedtuple('SpecialParams',
['count', 'win_id'])
def __init__(self, name, split, hide, instance, completion, modes, def __init__(self, name, split, hide, instance, completion, modes,
not_modes, needs_js, is_debug, ignore_args, not_modes, needs_js, is_debug, ignore_args,
@ -89,8 +95,8 @@ class Command:
self.namespace = None self.namespace = None
self._count = None self._count = None
self.pos_args = [] self.pos_args = []
has_count, desc, type_conv, name_conv = self._inspect_func() special_params, desc, type_conv, name_conv = self._inspect_func()
self.has_count = has_count self.special_params = special_params
self.desc = desc self.desc = desc
self._type_conv = type_conv self._type_conv = type_conv
self._name_conv = name_conv self._name_conv = name_conv
@ -164,20 +170,16 @@ class Command:
"""Inspect the function to get useful informations from it. """Inspect the function to get useful informations from it.
Return: Return:
A (has_count, desc, parser, type_conv) tuple. A (special_params, desc, parser, type_conv) tuple.
has_count: Whether the command supports a count. special_params: A SpecialParams namedtuple.
desc: The description of the command. desc: The description of the command.
type_conv: A mapping of args to type converter callables. type_conv: A mapping of args to type converter callables.
name_conv: A mapping of names to convert. name_conv: A mapping of names to convert.
""" """
type_conv = {} type_conv = {}
name_conv = {} name_conv = {}
special_params = {'count': None, 'win_id': None}
signature = inspect.signature(self.handler) signature = inspect.signature(self.handler)
has_count = 'count' in signature.parameters
if has_count and (signature.parameters['count'].default is
inspect.Parameter.empty):
raise TypeError("{}: handler has count parameter without "
"default!".format(self.name))
doc = inspect.getdoc(self.handler) doc = inspect.getdoc(self.handler)
if doc is not None: if doc is not None:
desc = doc.splitlines()[0].strip() desc = doc.splitlines()[0].strip()
@ -185,9 +187,34 @@ class Command:
desc = "" desc = ""
if not self.ignore_args: if not self.ignore_args:
for param in signature.parameters.values(): for param in signature.parameters.values():
if param.name in ('self', 'count', 'win_id'):
continue
annotation_info = self._parse_annotation(param) annotation_info = self._parse_annotation(param)
if param.name == 'self':
continue
special = annotation_info.special
if special == 'count':
if special_params['count'] is not None:
raise ValueError("Registered multiple parameters "
"({}/{}) as count!".format(
special_params['count'],
param.name))
if param.default is inspect.Parameter.empty:
raise TypeError("{}: handler has count parameter "
"without default!".format(self.name))
special_params['count'] = param.name
continue
elif special == 'win_id':
if special_params['win_id'] is not None:
raise ValueError("Registered multiple parameters "
"({}/{}) as win_id!".format(
special_params['win_id'],
param.name))
special_params['win_id'] = param.name
continue
elif special is None:
pass
else:
raise ValueError("{}: Invalid value '{}' for 'special' "
"annotation!".format(self.name, special))
typ = self._get_type(param, annotation_info) typ = self._get_type(param, annotation_info)
args, kwargs = self._param_to_argparse_args( args, kwargs = self._param_to_argparse_args(
param, annotation_info) param, annotation_info)
@ -199,7 +226,8 @@ class Command:
log.commands.vdebug('Adding arg {} of type {} -> {}'.format( log.commands.vdebug('Adding arg {} of type {} -> {}'.format(
param.name, typ, callsig)) param.name, typ, callsig))
self.parser.add_argument(*args, **kwargs) self.parser.add_argument(*args, **kwargs)
return has_count, desc, type_conv, name_conv special_params = self.SpecialParams(**special_params)
return special_params, desc, type_conv, name_conv
def _param_to_argparse_args(self, param, annotation_info): def _param_to_argparse_args(self, param, annotation_info):
"""Get argparse arguments for a parameter. """Get argparse arguments for a parameter.
@ -272,12 +300,13 @@ class Command:
flag: The short name/flag if overridden. flag: The short name/flag if overridden.
name: The long name if overridden. name: The long name if overridden.
""" """
info = {'kwargs': {}, 'typ': None, 'flag': None, 'name': None} info = {'kwargs': {}, 'typ': None, 'flag': None, 'name': None,
'special': None}
if param.annotation is not inspect.Parameter.empty: if param.annotation is not inspect.Parameter.empty:
log.commands.vdebug("Parsing annotation {}".format( log.commands.vdebug("Parsing annotation {}".format(
param.annotation)) param.annotation))
if isinstance(param.annotation, dict): if isinstance(param.annotation, dict):
for field in ('type', 'flag', 'name'): for field in ('type', 'flag', 'name', 'special'):
if field in param.annotation: if field in param.annotation:
info[field] = param.annotation[field] info[field] = param.annotation[field]
del param.annotation[field] del param.annotation[field]
@ -330,10 +359,10 @@ class Command:
args.append(param.default) args.append(param.default)
elif param.kind == inspect.Parameter.KEYWORD_ONLY: elif param.kind == inspect.Parameter.KEYWORD_ONLY:
if self._count is not None: if self._count is not None:
kwargs['count'] = self._count kwargs[param.name] = self._count
else: else:
raise TypeError("{}: invalid parameter type {} for argument " raise TypeError("{}: invalid parameter type {} for argument "
"'count'!".format(self.name, param.kind)) "{!r}!".format(self.name, param.kind, param.name))
def _get_win_id_arg(self, win_id, param, args, kwargs): def _get_win_id_arg(self, win_id, param, args, kwargs):
"""Add the win_id argument to a function call. """Add the win_id argument to a function call.
@ -347,10 +376,10 @@ class Command:
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
args.append(win_id) args.append(win_id)
elif param.kind == inspect.Parameter.KEYWORD_ONLY: elif param.kind == inspect.Parameter.KEYWORD_ONLY:
kwargs['win_id'] = win_id kwargs[param.name] = win_id
else: else:
raise TypeError("{}: invalid parameter type {} for argument " raise TypeError("{}: invalid parameter type {} for argument "
"'count'!".format(self.name, param.kind)) "{!r}!".format(self.name, param.kind, param.name))
def _get_param_name_and_value(self, param): def _get_param_name_and_value(self, param):
"""Get the converted name and value for an inspect.Parameter.""" """Get the converted name and value for an inspect.Parameter."""
@ -389,12 +418,12 @@ class Command:
# Special case for 'self'. # Special case for 'self'.
self._get_self_arg(win_id, param, args) self._get_self_arg(win_id, param, args)
continue continue
elif param.name == 'count': elif param.name == self.special_params.count:
# Special case for 'count'. # Special case for count parameter.
self._get_count_arg(param, args, kwargs) self._get_count_arg(param, args, kwargs)
continue continue
elif param.name == 'win_id': elif param.name == self.special_params.win_id:
# Special case for 'win_id'. # Special case for win_id parameter.
self._get_win_id_arg(win_id, param, args, kwargs) self._get_win_id_arg(win_id, param, args, kwargs)
continue continue
name, value = self._get_param_name_and_value(param) name, value = self._get_param_name_and_value(param)

View File

@ -116,7 +116,7 @@ class SearchRunner(QObject):
self._search(text, rev=True) self._search(text, rev=True)
@cmdutils.register(instance='search-runner', hide=True, scope='window') @cmdutils.register(instance='search-runner', hide=True, scope='window')
def search_next(self, count=1): def search_next(self, count: {'special': 'count'}=1):
"""Continue the search to the ([count]th) next term. """Continue the search to the ([count]th) next term.
Args: Args:
@ -127,7 +127,7 @@ class SearchRunner(QObject):
self.do_search.emit(self._text, self._flags) self.do_search.emit(self._text, self._flags)
@cmdutils.register(instance='search-runner', hide=True, scope='window') @cmdutils.register(instance='search-runner', hide=True, scope='window')
def search_prev(self, count=1): def search_prev(self, count: {'special': 'count'}=1):
"""Continue the search to the ([count]th) previous term. """Continue the search to the ([count]th) previous term.
Args: Args:

View File

@ -442,8 +442,9 @@ class ConfigManager(QObject):
@cmdutils.register(name='set', instance='config', @cmdutils.register(name='set', instance='config',
completion=[Completion.section, Completion.option, completion=[Completion.section, Completion.option,
Completion.value]) Completion.value])
def set_command(self, win_id, sectname: {'name': 'section'}, def set_command(self, win_id: {'special': 'win_id'},
optname: {'name': 'option'}, value=None, temp=False): sectname: {'name': 'section'}, optname: {'name': 'option'},
value=None, temp=False):
"""Set an option. """Set an option.
If the option name ends with '?', the value of the option is shown If the option name ends with '?', the value of the option is shown

View File

@ -30,7 +30,7 @@ from qutebrowser.config import style
@cmdutils.register(scope='window') @cmdutils.register(scope='window')
def later(ms: int, *command, win_id): def later(ms: int, *command, win_id: {'special': 'win_id'}):
"""Execute a command after some time. """Execute a command after some time.
Args: Args:
@ -60,7 +60,7 @@ def later(ms: int, *command, win_id):
@cmdutils.register(scope='window') @cmdutils.register(scope='window')
def repeat(times: int, *command, win_id): def repeat(times: int, *command, win_id: {'special': 'win_id'}):
"""Repeat a given command. """Repeat a given command.
Args: Args:

View File

@ -179,10 +179,10 @@ def _get_command_doc(name, cmd):
raise KeyError("No description for arg {} of command " raise KeyError("No description for arg {} of command "
"'{}'!".format(e, cmd.name)) "'{}'!".format(e, cmd.name))
if cmd.has_count: if cmd.special_parameters.count is not None:
output.append("") output.append("")
output.append("==== count") output.append("==== count")
output.append(parser.arg_descs['count']) output.append(parser.arg_descs[cmd.special_params.count])
output.append("") output.append("")
output.append("") output.append("")