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,
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
underscores stripped.

View File

@ -575,8 +575,8 @@ class CommandDispatcher:
widget.keyReleaseEvent(release_evt)
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window', count='count',
flags={'horizontal': 'x'})
scope='window', count='count')
@cmdutils.argument('horizontal', flag='x')
def scroll_perc(self, perc: {'type': float}=None, horizontal=False,
count=None):
"""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.
"""
import inspect
from qutebrowser.utils import qtutils, log
from qutebrowser.commands import command, cmdexc
@ -164,3 +166,36 @@ class register: # pylint: disable=invalid-name
cmd_dict[name] = cmd
aliases += names[1:]
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('_', '-')
ArgInfo = collections.namedtuple('ArgInfo', ['flag'])
class Command:
"""Base skeleton for a command.
@ -54,7 +57,6 @@ class Command:
win_id_arg: The name of the win_id parameter, or None.
flags_with_args: A list of flags which take an argument.
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.
_needs_js: Whether the command needs javascript enabled
_modes: The modes the command can be executed in.
@ -73,7 +75,7 @@ class Command:
def __init__(self, *, handler, name, instance=None, maxsplit=None,
hide=False, completion=None, modes=None, not_modes=None,
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,
win_id=None):
# 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._needs_js = needs_js
self._star_args_optional = star_args_optional
self._flags = flags or {}
self.debug = debug
self.ignore_args = ignore_args
self.handler = handler
@ -131,11 +132,6 @@ class Command:
raise ValueError("Got {} completions, but only {} "
"arguments!".format(len(self.completion),
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):
"""Check if the command is permitted to run currently.
@ -239,13 +235,21 @@ class Command:
if not self.ignore_args:
for param in signature.parameters.values():
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':
continue
if self._inspect_special_param(param):
continue
typ = self._get_type(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))
callsig = debug_utils.format_call(
self.parser.add_argument, args, kwargs,
@ -294,19 +298,25 @@ class Command:
kwargs['nargs'] = '?'
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.
Args:
param: The inspect.Parameter object to get the args for.
annotation_info: An AnnotationInfo tuple for the parameter.
arg_info: An ArgInfo tuple for the parameter or None
Return:
A list of args.
"""
args = []
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:
raise ValueError("Flag '{}' of parameter {} (command {}) must be "
"exactly 1 char!".format(shortname, name,

View File

@ -21,7 +21,7 @@
import pytest
from qutebrowser.commands import cmdutils, cmdexc, argparser
from qutebrowser.commands import cmdutils, cmdexc, argparser, command
class TestCheckOverflow:
@ -208,7 +208,8 @@ class TestRegister:
assert not parser.parse_args([]).arg
def test_flag_argument(self):
@cmdutils.register(flags={'arg': 'b'})
@cmdutils.register()
@cmdutils.argument('arg', flag='b')
def fun(arg=False):
"""Blah."""
pass
@ -218,9 +219,44 @@ class TestRegister:
with pytest.raises(argparser.ArgumentParserError):
parser.parse_args(['-a'])
def test_unknown_argument_in_flags(self):
with pytest.raises(ValueError):
@cmdutils.register(flags={'foobar': 'f'})
def fun():
def test_partial_arg(self):
"""Test with only some arguments decorated with @cmdutils.argument."""
@cmdutils.register()
@cmdutils.argument('arg1', flag='b')
def fun(arg1=False, arg2=False):
"""Blah."""
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