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:
parent
965a012db7
commit
3c586f34ff
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user