Add a @cmdutils.argument decorator

For now the only available keyword argument is 'flag' which customizes
the flag an argument will get.

See #637.
This commit is contained in:
Florian Bruhin 2016-05-10 18:58:23 +02:00
parent 965a012db7
commit 3c586f34ff
5 changed files with 112 additions and 19 deletions

View File

@ -439,6 +439,18 @@ then automatically checked. Possible values:
- A tuple of multiple types above: Any of these types are valid values, - A tuple of multiple types above: Any of these types are valid values,
e.g. `('foo', 'bar')` or `(int, 'foo')`. e.g. `('foo', 'bar')` or `(int, 'foo')`.
You can customize how an argument is handled using the `@cmdutils.argument`
decorator *after* `@cmdutils.register`. This can e.g. be used to customize the
flag an argument should get:
[source,python]
----
@cmdutils.register(...)
@cmdutils.arg('bar', flag='c')
def foo(bar):
...
----
The name of an argument will always be the parameter name, with any trailing The name of an argument will always be the parameter name, with any trailing
underscores stripped. underscores stripped.

View File

@ -575,8 +575,8 @@ class CommandDispatcher:
widget.keyReleaseEvent(release_evt) widget.keyReleaseEvent(release_evt)
@cmdutils.register(instance='command-dispatcher', hide=True, @cmdutils.register(instance='command-dispatcher', hide=True,
scope='window', count='count', scope='window', count='count')
flags={'horizontal': 'x'}) @cmdutils.argument('horizontal', flag='x')
def scroll_perc(self, perc: {'type': float}=None, horizontal=False, def scroll_perc(self, perc: {'type': float}=None, horizontal=False,
count=None): count=None):
"""Scroll to a specific percentage of the page. """Scroll to a specific percentage of the page.

View File

@ -24,6 +24,8 @@ Module attributes:
aliases: A list of all aliases, needed for doc generation. aliases: A list of all aliases, needed for doc generation.
""" """
import inspect
from qutebrowser.utils import qtutils, log from qutebrowser.utils import qtutils, log
from qutebrowser.commands import command, cmdexc from qutebrowser.commands import command, cmdexc
@ -164,3 +166,36 @@ class register: # pylint: disable=invalid-name
cmd_dict[name] = cmd cmd_dict[name] = cmd
aliases += names[1:] aliases += names[1:]
return func return func
class argument: # pylint: disable=invalid-name
"""Decorator to customize an argument for @cmdutils.register.
This could also be a function, but as a class (with a "wrong" name) it's
much cleaner to implement.
Attributes:
_argname: The name of the argument to handle.
_kwargs: Keyword arguments, valid ArgInfo members
"""
def __init__(self, argname, **kwargs):
self._argname = argname
self._kwargs = kwargs
def __call__(self, func):
if self._argname not in inspect.signature(func).parameters:
raise ValueError("{} has no argument {}!".format(func.__name__,
self._argname))
# Fill up args which weren't passed
for arg in command.ArgInfo._fields:
if arg not in self._kwargs:
self._kwargs[arg] = None
if not hasattr(func, 'qute_args'):
func.qute_args = {}
func.qute_args[self._argname] = command.ArgInfo(**self._kwargs)
return func

View File

@ -35,6 +35,9 @@ def arg_name(name):
return name.rstrip('_').replace('_', '-') return name.rstrip('_').replace('_', '-')
ArgInfo = collections.namedtuple('ArgInfo', ['flag'])
class Command: class Command:
"""Base skeleton for a command. """Base skeleton for a command.
@ -54,7 +57,6 @@ class Command:
win_id_arg: The name of the win_id parameter, or None. win_id_arg: The name of the win_id parameter, or None.
flags_with_args: A list of flags which take an argument. flags_with_args: A list of flags which take an argument.
no_cmd_split: If true, ';;' to split sub-commands is ignored. no_cmd_split: If true, ';;' to split sub-commands is ignored.
_flags: A mapping of argument names to alternative flags
_type_conv: A mapping of conversion functions for arguments. _type_conv: A mapping of conversion functions for arguments.
_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.
@ -73,7 +75,7 @@ class Command:
def __init__(self, *, handler, name, instance=None, maxsplit=None, def __init__(self, *, handler, name, instance=None, maxsplit=None,
hide=False, completion=None, modes=None, not_modes=None, hide=False, completion=None, modes=None, not_modes=None,
needs_js=False, debug=False, ignore_args=False, needs_js=False, debug=False, ignore_args=False,
deprecated=False, no_cmd_split=False, flags=None, deprecated=False, no_cmd_split=False,
star_args_optional=False, scope='global', count=None, star_args_optional=False, scope='global', count=None,
win_id=None): win_id=None):
# I really don't know how to solve this in a better way, I tried. # I really don't know how to solve this in a better way, I tried.
@ -102,7 +104,6 @@ class Command:
self._scope = scope self._scope = scope
self._needs_js = needs_js self._needs_js = needs_js
self._star_args_optional = star_args_optional self._star_args_optional = star_args_optional
self._flags = flags or {}
self.debug = debug self.debug = debug
self.ignore_args = ignore_args self.ignore_args = ignore_args
self.handler = handler self.handler = handler
@ -131,11 +132,6 @@ class Command:
raise ValueError("Got {} completions, but only {} " raise ValueError("Got {} completions, but only {} "
"arguments!".format(len(self.completion), "arguments!".format(len(self.completion),
len(args))) len(args)))
for argname in self._flags:
if argname not in args:
raise ValueError("Got argument {} in flags param, but no such "
"argument exists for {}".format(
argname, self.name))
def _check_prerequisites(self, win_id): def _check_prerequisites(self, win_id):
"""Check if the command is permitted to run currently. """Check if the command is permitted to run currently.
@ -239,13 +235,21 @@ class Command:
if not self.ignore_args: if not self.ignore_args:
for param in signature.parameters.values(): for param in signature.parameters.values():
annotation_info = self._parse_annotation(param) annotation_info = self._parse_annotation(param)
try:
arg_info = self.handler.qute_args[param.name]
except (KeyError, AttributeError):
arg_info = ArgInfo(**{name: None
for name in ArgInfo._fields})
if param.name == 'self': if param.name == 'self':
continue continue
if self._inspect_special_param(param): if self._inspect_special_param(param):
continue continue
typ = self._get_type(param, annotation_info) typ = self._get_type(param, annotation_info)
kwargs = self._param_to_argparse_kwargs(param, annotation_info) kwargs = self._param_to_argparse_kwargs(param, annotation_info)
args = self._param_to_argparse_args(param, annotation_info) args = self._param_to_argparse_args(param, annotation_info,
arg_info)
self._type_conv.update(self._get_typeconv(param, typ)) self._type_conv.update(self._get_typeconv(param, typ))
callsig = debug_utils.format_call( callsig = debug_utils.format_call(
self.parser.add_argument, args, kwargs, self.parser.add_argument, args, kwargs,
@ -294,19 +298,25 @@ class Command:
kwargs['nargs'] = '?' kwargs['nargs'] = '?'
return kwargs return kwargs
def _param_to_argparse_args(self, param, annotation_info): def _param_to_argparse_args(self, param, annotation_info, arg_info):
"""Get argparse positional arguments for a parameter. """Get argparse positional arguments for a parameter.
Args: Args:
param: The inspect.Parameter object to get the args for. param: The inspect.Parameter object to get the args for.
annotation_info: An AnnotationInfo tuple for the parameter. annotation_info: An AnnotationInfo tuple for the parameter.
arg_info: An ArgInfo tuple for the parameter or None
Return: Return:
A list of args. A list of args.
""" """
args = [] args = []
name = arg_name(param.name) name = arg_name(param.name)
shortname = self._flags.get(param.name, name[0])
if arg_info.flag is not None:
shortname = arg_info.flag
else:
shortname = name[0]
if len(shortname) != 1: if len(shortname) != 1:
raise ValueError("Flag '{}' of parameter {} (command {}) must be " raise ValueError("Flag '{}' of parameter {} (command {}) must be "
"exactly 1 char!".format(shortname, name, "exactly 1 char!".format(shortname, name,

View File

@ -21,7 +21,7 @@
import pytest import pytest
from qutebrowser.commands import cmdutils, cmdexc, argparser from qutebrowser.commands import cmdutils, cmdexc, argparser, command
class TestCheckOverflow: class TestCheckOverflow:
@ -208,7 +208,8 @@ class TestRegister:
assert not parser.parse_args([]).arg assert not parser.parse_args([]).arg
def test_flag_argument(self): def test_flag_argument(self):
@cmdutils.register(flags={'arg': 'b'}) @cmdutils.register()
@cmdutils.argument('arg', flag='b')
def fun(arg=False): def fun(arg=False):
"""Blah.""" """Blah."""
pass pass
@ -218,9 +219,44 @@ class TestRegister:
with pytest.raises(argparser.ArgumentParserError): with pytest.raises(argparser.ArgumentParserError):
parser.parse_args(['-a']) parser.parse_args(['-a'])
def test_unknown_argument_in_flags(self): def test_partial_arg(self):
with pytest.raises(ValueError): """Test with only some arguments decorated with @cmdutils.argument."""
@cmdutils.register(flags={'foobar': 'f'}) @cmdutils.register()
def fun(): @cmdutils.argument('arg1', flag='b')
def fun(arg1=False, arg2=False):
"""Blah.""" """Blah."""
pass pass
class TestArgument:
"""Test the @cmdutils.argument decorator."""
# pylint: disable=unused-variable
def _arginfo(self, **kwargs):
"""Helper method to get an ArgInfo tuple."""
for arg in command.ArgInfo._fields:
if arg not in kwargs:
kwargs[arg] = None
return command.ArgInfo(**kwargs)
def test_invalid_argument(self):
with pytest.raises(ValueError) as excinfo:
@cmdutils.argument('foo')
def fun(bar):
"""Blah."""
pass
assert str(excinfo.value) == "fun has no argument foo!"
def test_storage(self):
@cmdutils.argument('foo', flag='x')
@cmdutils.argument('bar', flag='y')
def fun(foo, bar):
"""Blah."""
pass
expected = {
'foo': self._arginfo(flag='x'),
'bar': self._arginfo(flag='y')
}
assert fun.qute_args == expected