Moderate refactoring and parser/adapter enhancements

This commit is contained in:
red-agent 2009-09-17 04:38:39 +03:00
parent 4dae0bde44
commit 1a327414ca
3 changed files with 182 additions and 63 deletions

View File

@ -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.

View File

@ -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):

View File

@ -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):
"""