Moderate refactoring and parser/adapter enhancements
This commit is contained in:
parent
4dae0bde44
commit
1a327414ca
|
@ -23,7 +23,7 @@ from types import FunctionType, UnicodeType, TupleType, ListType, BooleanType
|
|||
from inspect import getargspec
|
||||
from operator import itemgetter
|
||||
|
||||
class CommandInternalError(Exception):
|
||||
class InternalError(Exception):
|
||||
pass
|
||||
|
||||
class CommandError(Exception):
|
||||
|
@ -46,13 +46,14 @@ class Command(object):
|
|||
|
||||
ARG_USAGE_PATTERN = 'Usage: %s %s'
|
||||
|
||||
def __init__(self, handler, usage, source, raw, extra, empty, expand_short):
|
||||
def __init__(self, handler, usage, source, raw, extra, overlap, empty, expand_short):
|
||||
self.handler = handler
|
||||
|
||||
self.usage = usage
|
||||
self.source = source
|
||||
self.raw = raw
|
||||
self.extra = extra
|
||||
self.overlap = overlap
|
||||
self.empty = empty
|
||||
self.expand_short = expand_short
|
||||
|
||||
|
@ -120,7 +121,7 @@ class Command(object):
|
|||
# Removing self from arguments specification. Command handler should
|
||||
# normally be an instance method.
|
||||
if spec_args.pop(0) != 'self':
|
||||
raise CommandInternalError("First argument must be self")
|
||||
raise InternalError("First argument must be self")
|
||||
|
||||
return spec_args, spec_kwargs, var_args, var_kwargs
|
||||
|
||||
|
@ -142,9 +143,14 @@ class Command(object):
|
|||
|
||||
for key, value in spec_kwargs:
|
||||
letter = key[0]
|
||||
|
||||
key = key.replace('_', '-')
|
||||
value = ('=%s' % value) if not isinstance(value, BooleanType) else str()
|
||||
|
||||
if isinstance(value, BooleanType):
|
||||
value = str()
|
||||
elif isinstance(value, (TupleType, ListType)):
|
||||
value = '={%s}' % ', '.join(value)
|
||||
else:
|
||||
value = '=%s' % value
|
||||
|
||||
if letter not in letters:
|
||||
kwargs.append('-(-%s)%s%s' % (letter, key[1:], value))
|
||||
|
@ -155,9 +161,12 @@ class Command(object):
|
|||
usage = str()
|
||||
args = str()
|
||||
|
||||
if len(spec_args) == 1 and self.raw:
|
||||
args += ('(|%s|)' if self.empty else '|%s|') % spec_args[0]
|
||||
elif spec_args or var_args or sp_extra:
|
||||
if self.raw:
|
||||
spec_len = len(spec_args) - 1
|
||||
if spec_len:
|
||||
args += ('<%s>' % ', '.join(spec_args[:spec_len])) + ' '
|
||||
args += ('(|%s|)' if self.empty else '|%s|') % spec_args[-1]
|
||||
else:
|
||||
if spec_args:
|
||||
args += '<%s>' % ', '.join(spec_args)
|
||||
if var_args or sp_extra:
|
||||
|
@ -185,8 +194,10 @@ class Dispatcher(type):
|
|||
hosted = {}
|
||||
|
||||
def __init__(cls, name, bases, dct):
|
||||
Dispatcher.check_if_dispatchable(bases, dct)
|
||||
Dispatcher.check_if_hostable(bases, dct)
|
||||
dispatchable = Dispatcher.check_if_dispatchable(bases, dct)
|
||||
hostable = Dispatcher.check_if_hostable(bases, dct)
|
||||
|
||||
cls.check_if_conformed(dispatchable, hostable)
|
||||
|
||||
if Dispatcher.is_suitable(cls, dct):
|
||||
Dispatcher.register_processor(cls)
|
||||
|
@ -207,20 +218,27 @@ class Dispatcher(type):
|
|||
def check_if_dispatchable(cls, bases, dct):
|
||||
dispatcher = dct.get('DISPATCHED_BY')
|
||||
if not dispatcher:
|
||||
return
|
||||
return False
|
||||
if dispatcher not in bases:
|
||||
raise CommandInternalError("Should be dispatched by the same processor it inherits from")
|
||||
raise InternalError("Should be dispatched by the same processor it inherits from")
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def check_if_hostable(cls, bases, dct):
|
||||
hosters = dct.get('HOSTED_BY')
|
||||
if not hosters:
|
||||
return
|
||||
return False
|
||||
if not isinstance(hosters, (TupleType, ListType)):
|
||||
hosters = (hosters,)
|
||||
for hoster in hosters:
|
||||
if hoster not in bases:
|
||||
raise CommandInternalError("Should be hosted by the same processors it inherits from")
|
||||
raise InternalError("Should be hosted by the same processors it inherits from")
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def check_if_conformed(cls, dispatchable, hostable):
|
||||
if dispatchable and hostable:
|
||||
raise InternalError("Processor can not be dispatchable and hostable at the same time")
|
||||
|
||||
@classmethod
|
||||
def register_processor(cls, proc):
|
||||
|
@ -261,7 +279,7 @@ class Dispatcher(type):
|
|||
if name not in cls.table[proc]:
|
||||
cls.table[proc][name] = command
|
||||
else:
|
||||
raise CommandInternalError("Command with name %s already exists" % name)
|
||||
raise InternalError("Command with name %s already exists" % name)
|
||||
@classmethod
|
||||
def register_adhocs(cls, proc):
|
||||
hosters = proc.HOSTED_BY
|
||||
|
@ -421,6 +439,13 @@ class CommandProcessor(object):
|
|||
position = match.span()
|
||||
args.append((body, position))
|
||||
|
||||
# In rare occasions quoted options are being captured, while they should
|
||||
# not be. This fixes the problem by finding options which intersect with
|
||||
# arguments and removing them.
|
||||
for key, value, position in opts[:]:
|
||||
if intersects_args(position):
|
||||
opts.remove((key, value, position))
|
||||
|
||||
return args, opts
|
||||
|
||||
@classmethod
|
||||
|
@ -441,20 +466,85 @@ class CommandProcessor(object):
|
|||
cases) then this option will be treated as a switch, that is an option
|
||||
which does not take an argument. Argument preceded by a switch will be
|
||||
treated just like a normal positional argument.
|
||||
|
||||
If keyword argument's initial value is a sequence (tuple or a string)
|
||||
then possible values of the option will be restricted to one of the
|
||||
values given by the sequence.
|
||||
"""
|
||||
spec_args, spec_kwargs, var_args, var_kwargs = command.extract_arg_spec()
|
||||
spec_kwargs = dict(spec_kwargs)
|
||||
norm_kwargs = dict(spec_kwargs)
|
||||
|
||||
# Quite complex piece of neck-breaking logic to extract raw arguments if
|
||||
# there is more, then one positional argument specified by the command.
|
||||
# In case if it's just one argument which is the collector this is
|
||||
# fairly easy. But when it's more then one argument - the neck-breaking
|
||||
# logic of how to retrieve residual arguments as a raw, all in one piece
|
||||
# string, kicks on.
|
||||
if command.raw:
|
||||
if len(spec_args) == 1 and not spec_kwargs and not var_args and not var_kwargs:
|
||||
if arguments or command.empty:
|
||||
return (arguments,), {}
|
||||
raise CommandError("Can not be used without arguments", command)
|
||||
raise CommandInternalError("Raw command must define no more then one argument")
|
||||
if spec_kwargs or var_args or var_kwargs:
|
||||
raise InternalError("Raw commands should define only positional arguments")
|
||||
|
||||
if arguments:
|
||||
spec_fix = 1 if command.source else 0
|
||||
spec_len = len(spec_args) - spec_fix
|
||||
arguments_end = len(arguments) - 1
|
||||
|
||||
# If there are any optional arguments given they should be
|
||||
# either an unquoted postional argument or part of the raw
|
||||
# argument. So we find all optional arguments that can possibly
|
||||
# be unquoted argument and append them as is to the args.
|
||||
for key, value, (start, end) in opts[:spec_len]:
|
||||
if value:
|
||||
end -= len(value) + 1
|
||||
args.append((arguments[start:end], (start, end)))
|
||||
args.append((value, (end, end + len(value) + 1)))
|
||||
else:
|
||||
args.append((arguments[start:end], (start, end)))
|
||||
|
||||
# We need in-place sort here because after manipulations with
|
||||
# options order of arguments might be wrong and we just can't
|
||||
# have more complex logic to not let that happen.
|
||||
args.sort(key=itemgetter(1))
|
||||
|
||||
if spec_len > 1:
|
||||
stopper, (start, end) = args[spec_len - 2]
|
||||
|
||||
raw = arguments[end:]
|
||||
raw = raw.strip() or None
|
||||
|
||||
if not raw and not command.empty:
|
||||
raise CommandError("Missing arguments", command)
|
||||
|
||||
# Discard residual arguments and all of the options as raw
|
||||
# command does not support options and if an option is given
|
||||
# it is rather a part of a raw argument.
|
||||
args = args[:spec_len - 1]
|
||||
opts = []
|
||||
|
||||
args.append((raw, (end, arguments_end)))
|
||||
elif spec_len == 1:
|
||||
args = [(arguments, (0, arguments_end))]
|
||||
else:
|
||||
raise InternalError("Raw command must define a collector")
|
||||
else:
|
||||
if command.empty:
|
||||
args.append((None, (0, 0)))
|
||||
else:
|
||||
raise CommandError("Missing arguments", command)
|
||||
|
||||
# The first stage of transforming options we have got to a format that
|
||||
# can be used to associate them with declared keyword arguments.
|
||||
# Substituting dashes (-) in their names with underscores (_).
|
||||
for index, (key, value, position) in enumerate(opts):
|
||||
if '-' in key:
|
||||
opts[index] = (key.replace('-', '_'), value, position)
|
||||
|
||||
# The second stage of transforming options to an associatable state.
|
||||
# Expanding short, one-letter options to a verbose ones, if
|
||||
# corresponding optin has been given.
|
||||
if command.expand_short:
|
||||
expanded = []
|
||||
for spec_key, spec_value in spec_kwargs.iteritems():
|
||||
for spec_key, spec_value in norm_kwargs.iteritems():
|
||||
letter = spec_key[0] if len(spec_key) > 1 else None
|
||||
if letter and letter not in expanded:
|
||||
for index, (key, value, position) in enumerate(opts):
|
||||
|
@ -463,8 +553,10 @@ class CommandProcessor(object):
|
|||
opts[index] = (spec_key, value, position)
|
||||
break
|
||||
|
||||
# Detect switches and set their values accordingly. If any of them
|
||||
# carries a value - append it to args.
|
||||
for index, (key, value, position) in enumerate(opts):
|
||||
if isinstance(spec_kwargs.get(key), BooleanType):
|
||||
if isinstance(norm_kwargs.get(key), BooleanType):
|
||||
opts[index] = (key, True, position)
|
||||
if value:
|
||||
args.append((value, position))
|
||||
|
@ -479,18 +571,40 @@ class CommandProcessor(object):
|
|||
args = map(lambda (arg, position): arg, args)
|
||||
opts = map(lambda (key, value, position): (key, value), opts)
|
||||
|
||||
# If command has extra option enabled - collect all extra arguments and
|
||||
# pass them to a last positional argument command defines as a list.
|
||||
if command.extra:
|
||||
if not var_args:
|
||||
positional_len = len(spec_args) - (1 if not command.source else 2)
|
||||
extra = args[positional_len:]
|
||||
args = args[:positional_len]
|
||||
spec_fix = 1 if not command.source else 2
|
||||
spec_len = len(spec_args) - spec_fix
|
||||
extra = args[spec_len:]
|
||||
args = args[:spec_len]
|
||||
args.append(extra)
|
||||
else:
|
||||
raise CommandInternalError("Can not have both, extra and *args")
|
||||
raise InternalError("Can not have both, extra and *args")
|
||||
|
||||
for index, (key, value) in enumerate(opts):
|
||||
if '-' in key:
|
||||
opts[index] = (key.replace('-', '_'), value)
|
||||
# Detect if positional arguments overlap keyword arguments. If so and
|
||||
# this is allowed by command options - then map them directly to their
|
||||
# options, so they can get propert further processings.
|
||||
spec_fix = 1 if command.source else 0
|
||||
spec_len = len(spec_args) - spec_fix
|
||||
if len(args) > spec_len:
|
||||
if command.overlap:
|
||||
overlapped = args[spec_len:]
|
||||
args = args[:spec_len]
|
||||
for arg, (spec_key, spec_value) in zip(overlapped, spec_kwargs):
|
||||
opts.append((spec_key, arg))
|
||||
else:
|
||||
raise CommandError("Excessive arguments", command)
|
||||
|
||||
# Detect every contraint sequences and ensure that if corresponding
|
||||
# options are given - they contain proper values, within constraint
|
||||
# range.
|
||||
for key, value in opts:
|
||||
initial = norm_kwargs.get(key)
|
||||
if isinstance(initial, (TupleType, ListType)) and value not in initial:
|
||||
if value:
|
||||
raise CommandError("Wrong argument", command)
|
||||
|
||||
# We need to encode every keyword argument to a simple string, not the
|
||||
# unicode one, because ** expansion does not support it.
|
||||
|
@ -498,9 +612,13 @@ class CommandProcessor(object):
|
|||
if isinstance(key, UnicodeType):
|
||||
opts[index] = (key.encode(cls.ARG_ENCODING), value)
|
||||
|
||||
# Inject the source arguments as a string as a first argument, if
|
||||
# command has enabled the corresponding option.
|
||||
if command.source:
|
||||
args.insert(0, arguments)
|
||||
|
||||
# Return *args and **kwargs in the form suitable for passing to a
|
||||
# command handlers and being expanded.
|
||||
return tuple(args), dict(opts)
|
||||
|
||||
def process_as_command(self, text):
|
||||
|
@ -515,11 +633,7 @@ class CommandProcessor(object):
|
|||
text = text.strip()
|
||||
|
||||
parts = text.split(' ', 1)
|
||||
|
||||
if len(parts) > 1:
|
||||
name, arguments = parts
|
||||
else:
|
||||
name, arguments = parts[0], None
|
||||
name, arguments = parts if len(parts) > 1 else (parts[0], None)
|
||||
|
||||
flag = self.looks_like_command(text, name, arguments)
|
||||
if flag is not None:
|
||||
|
@ -593,6 +707,10 @@ def command(*names, **kwargs):
|
|||
cases only because of some Python limitations on this - arguments can't be
|
||||
mapped correctly when there are keyword arguments present.
|
||||
|
||||
If overlap=True is given - then if extra=False and there is extra arguments
|
||||
given to the command - they will be mapped as if they were values for the
|
||||
keyword arguments, in the order they are defined.
|
||||
|
||||
If expand_short=True is given - then if command receives one-letter
|
||||
options (like -v or -f) they will be expanded to a verbose ones (like
|
||||
--verbose or --file) if the latter are defined as a command optional
|
||||
|
@ -607,11 +725,15 @@ def command(*names, **kwargs):
|
|||
source = kwargs.get('source', False)
|
||||
raw = kwargs.get('raw', False)
|
||||
extra = kwargs.get('extra', False)
|
||||
overlap = kwargs.get('overlap', False)
|
||||
empty = kwargs.get('empty', False)
|
||||
expand_short = kwargs.get('expand_short', True)
|
||||
|
||||
if extra and overlap:
|
||||
raise InternalError("Extra and overlap options can not be used together")
|
||||
|
||||
def decorator(handler):
|
||||
command = Command(handler, usage, source, raw, extra, empty, expand_short)
|
||||
command = Command(handler, usage, source, raw, extra, overlap, empty, expand_short)
|
||||
|
||||
# Extract and inject native name while making sure it is going to be the
|
||||
# first one in the list.
|
||||
|
|
|
@ -46,7 +46,7 @@ class CommonCommands(ChatMiddleware):
|
|||
"""
|
||||
self.chat_buttons_set_visible(not self.hide_chat_buttons)
|
||||
|
||||
@command
|
||||
@command(overlap=True)
|
||||
def help(self, command=None, all=False):
|
||||
"""
|
||||
Show help on a given command or a list of available commands if -(-a)ll is
|
||||
|
@ -85,6 +85,15 @@ class CommonCommands(ChatMiddleware):
|
|||
"""
|
||||
self.send("/me %s" % action)
|
||||
|
||||
@command(raw=True, empty=True)
|
||||
def test(self, one, two, three):
|
||||
self.echo(one)
|
||||
self.echo(two)
|
||||
self.echo(three)
|
||||
|
||||
from pprint import pformat
|
||||
return "Locals:\n%s" % pformat(locals())
|
||||
|
||||
class ChatCommands(CommonCommands):
|
||||
"""
|
||||
Here defined commands will be unique to a chat. Use it as a hoster to provide
|
||||
|
@ -147,13 +156,12 @@ class GroupChatCommands(CommonCommands):
|
|||
else:
|
||||
raise CommandError(_("Nickname not found"))
|
||||
|
||||
@command('msg')
|
||||
def message(self, nick, *a_message):
|
||||
@command('msg', raw=True)
|
||||
def message(self, nick, a_message):
|
||||
"""
|
||||
Open a private chat window with a specified occupant and send him a
|
||||
message
|
||||
"""
|
||||
a_message = self.collect(a_message, False)
|
||||
nicks = gajim.contacts.get_nick_list(self.account, self.room_jid)
|
||||
if nick in nicks:
|
||||
self.on_send_pm(nick=nick, msg=a_message)
|
||||
|
@ -170,21 +178,21 @@ class GroupChatCommands(CommonCommands):
|
|||
else:
|
||||
return self.subject
|
||||
|
||||
@command
|
||||
def invite(self, jid, *reason):
|
||||
@command(raw=True, empty=True)
|
||||
def invite(self, jid, reason):
|
||||
"""
|
||||
Invite a user to a room for a reason
|
||||
"""
|
||||
reason = self.collect(reason)
|
||||
self.connection.send_invite(self.room_jid, jid, reason)
|
||||
return _("Invited %s to %s") % (jid, self.room_jid)
|
||||
|
||||
@command
|
||||
def join(self, jid, *nick):
|
||||
@command(raw=True, empty=True)
|
||||
def join(self, jid, nick):
|
||||
"""
|
||||
Join a group chat given by a jid, optionally using given nickname
|
||||
"""
|
||||
nick = self.collect(nick) or self.nick
|
||||
if not nick:
|
||||
nick = self.nick
|
||||
|
||||
if '@' not in jid:
|
||||
jid = jid + '@' + gajim.get_server_from_jid(self.room_jid)
|
||||
|
@ -204,28 +212,26 @@ class GroupChatCommands(CommonCommands):
|
|||
"""
|
||||
self.parent_win.remove_tab(self, self.parent_win.CLOSE_COMMAND, reason)
|
||||
|
||||
@command
|
||||
def ban(self, who, *reason):
|
||||
@command(raw=True, empty=True)
|
||||
def ban(self, who, reason):
|
||||
"""
|
||||
Ban user by a nick or a jid from a groupchat
|
||||
|
||||
If given nickname is not found it will be treated as a jid.
|
||||
"""
|
||||
reason = self.collect(reason, none=False)
|
||||
if who in gajim.contacts.get_nick_list(self.account, self.room_jid):
|
||||
contact = gajim.contacts.get_gc_contact(self.account, self.room_jid, who)
|
||||
who = contact.jid
|
||||
self.connection.gc_set_affiliation(self.room_jid, who, 'outcast', reason)
|
||||
self.connection.gc_set_affiliation(self.room_jid, who, 'outcast', reason or str())
|
||||
|
||||
@command
|
||||
def kick(self, who, *reason):
|
||||
@command(raw=True, empty=True)
|
||||
def kick(self, who, reason):
|
||||
"""
|
||||
Kick user by a nick from a groupchat
|
||||
"""
|
||||
reason = self.collect(reason, none=False)
|
||||
if not who in gajim.contacts.get_nick_list(self.account, self.room_jid):
|
||||
raise CommandError(_("Nickname not found"))
|
||||
self.connection.gc_set_role(self.room_jid, who, 'none', reason)
|
||||
self.connection.gc_set_role(self.room_jid, who, 'none', reason or str())
|
||||
|
||||
@command
|
||||
def names(self, verbose=False):
|
||||
|
|
|
@ -97,15 +97,6 @@ class ChatMiddleware(CommandProcessor):
|
|||
"""
|
||||
self.save_sent_message(text)
|
||||
|
||||
def collect(self, arguments, empty=True, separator=' ', none=True):
|
||||
"""
|
||||
Might come in handy in case if you want to map some arguments and
|
||||
collect the rest of them into a string.
|
||||
"""
|
||||
if not empty and not arguments:
|
||||
raise CommandError(_("Missing argument"))
|
||||
return None if not arguments and none else separator.join(arguments)
|
||||
|
||||
@property
|
||||
def connection(self):
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue