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,
|
- 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.
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user