Dropped in the reworked version of the new command system

The reworked version has refactored and simplified architecture, which also
involves simplified dispatching.
This commit is contained in:
red-agent 2009-10-02 23:57:11 +03:00
parent ca127b8d31
commit 958d937d5c
15 changed files with 1055 additions and 937 deletions

View File

@ -51,7 +51,13 @@ from common.pep import MOODS, ACTIVITIES
from common.xmpp.protocol import NS_XHTML, NS_XHTML_IM, NS_FILE, NS_MUC
from common.xmpp.protocol import NS_RECEIPTS, NS_ESESSION
from commands.implementation import CommonCommands, ChatCommands
from command_system.implementation.middleware import ChatCommandProcessor
from command_system.implementation.middleware import CommandTools
from command_system.implementation.hosts import ChatCommands
# Here we load the module with the standard commands, so they are being detected
# and dispatched.
import command_system.implementation.standard
try:
import gtkspell
@ -81,7 +87,7 @@ if gajim.config.get('use_speller') and HAS_GTK_SPELL:
del tv
################################################################################
class ChatControlBase(MessageControl, CommonCommands):
class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
'''A base class containing a banner, ConversationTextview, MessageTextView
'''
@ -1164,12 +1170,14 @@ class ChatControlBase(MessageControl, CommonCommands):
# FIXME: Set sensitivity for toolbar
################################################################################
class ChatControl(ChatControlBase, ChatCommands):
class ChatControl(ChatControlBase):
'''A control for standard 1-1 chat'''
TYPE_ID = message_control.TYPE_CHAT
old_msg_kind = None # last kind of the printed message
DISPATCHED_BY = ChatCommands
# Set a command host to bound to. Every command given through a chat will be
# processed with this command host.
COMMAND_HOST = ChatCommands
def __init__(self, parent_win, contact, acct, session, resource = None):
ChatControlBase.__init__(self, self.TYPE_ID, parent_win,

View File

@ -0,0 +1,20 @@
# Copyright (C) 2009 red-agent <hell.director@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
The command system providing scalable, clean and convenient architecture in
combination with declarative way of defining commands and a fair amount of
automatization for routine processes.
"""

View File

@ -0,0 +1,89 @@
# Copyright (C) 2009 red-agent <hell.director@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
The backbone of the command system. Provides automatic dispatching which does
not require explicit registering commands or containers and remains active even
after everything is done, so new commands can be added during the runtime.
"""
from types import NoneType
class Dispatcher(type):
containers = {}
commands = {}
@classmethod
def register_host(klass, host):
klass.containers[host] = []
@classmethod
def register_container(klass, container):
for host in container.HOSTS:
klass.containers[host].append(container)
@classmethod
def register_commands(klass, container):
klass.commands[container] = {}
for command in klass.traverse_commands(container):
for name in command.names:
klass.commands[container][name] = command
@classmethod
def get_command(klass, host, name):
for container in klass.containers[host]:
command = klass.commands[container].get(name)
if command:
return command
@classmethod
def list_commands(klass, host):
for container in klass.containers[host]:
commands = klass.commands[container]
for name, command in commands.iteritems():
yield name, command
@classmethod
def traverse_commands(klass, container):
for name in dir(container):
attribute = getattr(container, name)
if klass.is_command(attribute):
yield attribute
@staticmethod
def is_root(ns):
meta = ns.get('__metaclass__', NoneType)
return issubclass(meta, Dispatcher)
@staticmethod
def is_command(attribute):
name = attribute.__class__.__name__
return name == 'Command'
class HostDispatcher(Dispatcher):
def __init__(klass, name, bases, ns):
if not Dispatcher.is_root(ns):
HostDispatcher.register_host(klass)
super(HostDispatcher, klass).__init__(name, bases, ns)
class ContainerDispatcher(Dispatcher):
def __init__(klass, name, bases, ns):
if not Dispatcher.is_root(ns):
ContainerDispatcher.register_container(klass)
ContainerDispatcher.register_commands(klass)
super(ContainerDispatcher, klass).__init__(name, bases, ns)

View File

@ -0,0 +1,41 @@
# Copyright (C) 2009 red-agent <hell.director@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class BaseError(Exception):
"""
Common base for errors which relate to a specific command. Encapsulates
everything needed to identify a command, by either its object or name.
"""
def __init__(self, message, command=None, name=None):
self.command = command
self.name = name
if command and not name:
self.name = command.first_name
super(BaseError, self).__init__(message)
class DefinitionError(BaseError):
"""
Used to indicate errors occured on command definition.
"""
pass
class CommandError(BaseError):
"""
Used to indicate errors occured during command execution.
"""
pass

View File

@ -0,0 +1,320 @@
# Copyright (C) 2009 red-agent <hell.director@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Provides a tiny framework with simple, yet powerful and extensible architecture
to implement commands in a streight and flexible, declarative way.
"""
import re
from types import FunctionType
from inspect import getargspec
from dispatching import Dispatcher, HostDispatcher, ContainerDispatcher
from mapping import parse_arguments, adapt_arguments
from errors import DefinitionError, CommandError
class CommandHost(object):
"""
Command host is a hub between numerous command processors and command
containers. Aimed to participate in a dispatching process in order to
provide clean and transparent architecture.
"""
__metaclass__ = HostDispatcher
class CommandContainer(object):
"""
Command container is an entity which holds defined commands, allowing them
to be dispatched and proccessed correctly. Each command container may be
bound to a one or more command hosts.
Bounding is controlled by the HOSTS variable, which must be defined in the
body of the command container. This variable should contain a list of hosts
to bound to, as a tuple or list.
"""
__metaclass__ = ContainerDispatcher
class CommandProcessor(object):
"""
Command processor is an immediate command emitter. It does not participate
in the dispatching process directly, but must define a host to bound to.
Bounding is controlled by the COMMAND_HOST variable, which must be defined
in the body of the command processor. This variable should be set to a
specific command host.
"""
# This defines a command prefix (or an initializer), which should preceede a
# a text in order it to be processed as a command.
COMMAND_PREFIX = '/'
def process_as_command(self, text):
"""
Try to process text as a command. Returns True if it has been processed
as a command and False otherwise.
"""
if not text.startswith(self.COMMAND_PREFIX):
return False
body = text[len(self.COMMAND_PREFIX):]
body = body.strip()
parts = body.split(None, 1)
name, arguments = parts if len(parts) > 1 else (parts[0], None)
flag = self.looks_like_command(text, body, name, arguments)
if flag is not None:
return flag
self.execute_command(name, arguments)
return True
def execute_command(self, name, arguments):
command = self.get_command(name)
args, opts = parse_arguments(arguments) if arguments else ([], [])
args, kwargs = adapt_arguments(command, arguments, args, opts)
if self.command_preprocessor(command, name, arguments, args, kwargs):
return
value = command(self, *args, **kwargs)
self.command_postprocessor(command, name, arguments, args, kwargs, value)
def command_preprocessor(self, command, name, arguments, args, kwargs):
"""
Redefine this method in the subclass to execute custom code before
command gets executed.
If returns True then command execution will be interrupted and command
will not be executed.
"""
pass
def command_postprocessor(self, command, name, arguments, args, kwargs, value):
"""
Redefine this method in the subclass to execute custom code after
command gets executed.
"""
pass
def looks_like_command(self, text, body, name, arguments):
"""
This hook is being called before any processing, but after it was
determined that text looks like a command.
If returns value other then None - then further processing will be
interrupted and that value will be used to return from
process_as_command.
"""
pass
def get_command(self, name):
command = Dispatcher.get_command(self.COMMAND_HOST, name)
if not command:
raise CommandError("Command does not exist", name=name)
return command
def list_commands(self):
commands = Dispatcher.list_commands(self.COMMAND_HOST)
commands = dict(commands)
return sorted(set(commands.itervalues()))
class Command(object):
# These two regular expression patterns control how command documentation
# will be formatted to be transformed to a normal, readable state.
DOC_STRIP_PATTERN = re.compile(r'(?:^[ \t]+|\A\n)', re.MULTILINE)
DOC_FORMAT_PATTERN = re.compile(r'(?<!\n)\n(?!\n)', re.MULTILINE)
def __init__(self, handler, *names, **properties):
self.handler = handler
self.names = names
# Automatically set all the properties passed to a constructor by the
# command decorator.
for key, value in properties.iteritems():
setattr(self, key, value)
def __call__(self, *args, **kwargs):
try:
return self.handler(*args, **kwargs)
# This allows to use a shortcuted way of raising an exception inside a
# handler. That is to raise a CommandError without command or name
# attributes set. They will be set to a corresponding values right here
# in case if they was not set by the one who raised an exception.
except CommandError, error:
if not error.command and not error.name:
raise CommandError(exception.message, self)
raise
# This one is a little bit too wide, but as Python does not have
# anything more constrained - there is no other choice. Take a look here
# if command complains about invalid arguments while they are ok.
except TypeError:
raise CommandError("Command received invalid arguments", self)
def __repr__(self):
return "<Command %s>" % ', '.join(self.names)
def __cmp__(self, other):
return cmp(self.first_name, other.first_name)
@property
def first_name(self):
return self.names[0]
@property
def native_name(self):
return self.handler.__name__
def extract_documentation(self):
"""
Extract handler's documentation which is a doc-string and transform it
to a usable format.
Transformation is done based on the DOC_STRIP_PATTERN and
DOC_FORMAT_PATTERN regular expression patterns.
"""
documentation = self.handler.__doc__ or None
if not documentation:
return
documentation = re.sub(self.DOC_STRIP_PATTERN, str(), documentation)
documentation = re.sub(self.DOC_FORMAT_PATTERN, ' ', documentation)
return documentation
def extract_description(self):
"""
Extract handler's description (which is a first line of the
documentation). Try to keep them simple yet meaningful.
"""
documentation = self.extract_documentation()
return documentation.split('\n', 1)[0] if documentation else None
def extract_specification(self):
"""
Extract handler's arguments specification, as it was defined preserving
their order.
"""
names, var_args, var_kwargs, defaults = getargspec(self.handler)
# Behavior of this code need to be checked. Might yield incorrect
# results on some rare occasions.
spec_args = names[:-len(defaults) if defaults else len(names)]
spec_kwargs = list(zip(names[-len(defaults):], defaults)) if defaults else {}
# Removing self from arguments specification. Command handler should
# receive the processors as a first argument, which should be self by
# the canonical means.
if spec_args.pop(0) != 'self':
raise DefinitionError("First argument must be self", self)
return spec_args, spec_kwargs, var_args, var_kwargs
def command(*names, **properties):
"""
A decorator for defining commands in a declarative way. Provides facilities
for setting command's names and properties.
Names should contain a set of names (aliases) by which the command can be
reached. If no names are given - the the native name (the one extracted from
the command handler) will be used.
If include_native=True is given (default) and names is non-empty - then the
native name of the command will be prepended in addition to the given names.
If usage=True is given (default) - then command help will be appended with
autogenerated usage info, based of the command handler arguments
introspection.
If source=True is given - then the first argument of the command will
receive the source arguments, as a raw, unprocessed string. The further
mapping of arguments and options will not be affected.
If raw=True is given - then command considered to be raw and should define
positional arguments only. If it defines only one positional argument - this
argument will receive all the raw and unprocessed arguments. If the command
defines more then one positional argument - then all the arguments except
the last one will be processed normally; the last argument will get what is
left after the processing as raw and unprocessed string.
If empty=True is given - this will allow to call a raw command without
arguments.
If extra=True is given - then all the extra arguments passed to a command
will be collected into a sequence and given to the last positional argument.
If overlap=True is given - then all the extra arguments will be mapped as if
they were values for the keyword arguments.
If expand_short=True is given (default) - then short, one-letter options
will be expanded to a verbose ones, based of the comparison of the first
letter. If more then one option with the same first letter is given - then
only first one will be used in the expansion.
"""
names = list(names)
include_native = properties.get('include_native', True)
usage = properties.get('usage', True)
source = properties.get('source', False)
raw = properties.get('raw', False)
empty = properties.get('empty', False)
extra = properties.get('extra', False)
overlap = properties.get('overlap', False)
expand_short = properties.get('expand_short', True)
if empty and not raw:
raise DefinitionError("Empty option can be used only with raw commands")
if extra and overlap:
raise DefinitionError("Extra and overlap options can not be used together")
properties = {
'usage': usage,
'source': source,
'raw': raw,
'extra': extra,
'overlap': overlap,
'empty': empty,
'expand_short': expand_short
}
def decorator(handler):
"""
Decorator which receives handler as a first argument and then wraps it
in the command which then returns back.
"""
command = Command(handler, *names, **properties)
# Extract and inject a native name if either no other names are
# specified or include_native property is enabled, while making sure it
# is going to be the first one in the list.
if not names or include_native:
names.insert(0, command.native_name)
command.names = tuple(names)
return command
# Workaround if we are getting called without parameters. Keep in mind that
# in that case - first item in the names will be the handler.
if names and isinstance(names[0], FunctionType):
return decorator(names.pop(0))
return decorator

View File

@ -14,7 +14,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
The command system providing scalable and convenient architecture in combination
with declarative way of defining commands and a fair amount of automatization
for routine processes.
The implementation and auxilary systems which implement the standard Gajim
commands and also provide an infrastructure for adding custom commands.
"""

View File

@ -0,0 +1,85 @@
# Copyright (C) 2009 red-agent <hell.director@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
The module contains examples of how to create your own commands, by creating a
new command container and definding a set of commands.
Keep in mind that this module is not being loaded, so the code will not be
executed and commands defined here will not be detected.
"""
from ..framework import CommandContainer, command
from hosts import ChatCommands, PrivateChatCommands, GroupChatCommands
class CustomCommonCommands(CommandContainer):
"""
This command container bounds to all three available in the default
implementation command hosts. This means that commands defined in this
container will be available to all - chat, private chat and a group chat.
"""
HOSTS = (ChatCommands, PrivateChatCommands, GroupChatCommands)
@command
def dance(self):
"""
First line of the doc string is called a description and will be
programmatically extracted and formatted.
After that you can give more help, like explanation of the options. This
one will be programatically extracted and formatted too.
After all the documentation - there will be autogenerated (based on the
method signature) usage information appended. You can turn it off
though, if you want.
"""
return "I can't dance, you stupid fuck, I'm just a command system! A cool one, though..."
class CustomChatCommands(CommandContainer):
"""
This command container bounds only to the ChatCommands command host.
Therefore command defined here will be available only to a chat.
"""
HOSTS = (ChatCommands,)
@command
def sing(self):
return "Are you phreaking kidding me? Buy yourself a damn stereo..."
class CustomPrivateChatCommands(CommandContainer):
"""
This command container bounds only to the PrivateChatCommands command host.
Therefore command defined here will be available only to a private chat.
"""
HOSTS = (PrivateChatCommands,)
@command
def make_coffee(self):
return "What do I look like, you ass? A coffee machine!?"
class CustomGroupChatCommands(CommandContainer):
"""
This command container bounds only to the GroupChatCommands command host.
Therefore command defined here will be available only to a group chat.
"""
HOSTS = (GroupChatCommands,)
@command
def fetch(self):
return "You should really buy yourself a dog and start torturing it instead of me..."

View File

@ -0,0 +1,42 @@
# Copyright (C) 2009 red-agent <hell.director@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
The module defines a set of command hosts, which are bound to a different
command processors, which are the source of commands.
"""
from ..framework import CommandHost
class ChatCommands(CommandHost):
"""
This command host is bound to the command processor which processes commands
from a chat.
"""
pass
class PrivateChatCommands(CommandHost):
"""
This command host is bound to the command processor which processes commands
from a private chat.
"""
pass
class GroupChatCommands(CommandHost):
"""
This command host is bound to the command processor which processes commands
from a group chat.
"""
pass

View File

@ -16,53 +16,70 @@
"""
Provides a glue to tie command system framework and the actual code where it
would be dropped in. Defines a little bit of scaffolding to support interaction
between the two and a few utility methods so you don't need to dig up the host
code to write basic commands.
between the two and a few utility methods so you don't need to dig up the code
itself code to write basic commands.
"""
from common import gajim
from types import StringTypes
from framework import CommandProcessor, CommandError
from traceback import print_exc
class ChatMiddleware(CommandProcessor):
from common import gajim
from ..framework import CommandProcessor
from ..errors import CommandError
class ChatCommandProcessor(CommandProcessor):
"""
Provides basic scaffolding for the convenient interaction with ChatControl.
Also provides some few basic utilities for the same purpose.
A basic scaffolding to provide convenient interaction between the command
system and chat controls.
"""
def execute_command(self, text, name, arguments):
def process_as_command(self, text):
flag = super(ChatCommandProcessor, self).process_as_command(text)
if flag:
self.add_history(text)
self.clear_input()
return flag
def execute_command(self, name, arguments):
try:
super(ChatMiddleware, self).execute_command(text, name, arguments)
except CommandError, exception:
self.echo("%s: %s" %(exception.name, exception.message), 'error')
super(ChatCommandProcessor, self).execute_command(name, arguments)
except CommandError, error:
self.echo("%s: %s" %(error.name, error.message), 'error')
except Exception:
self.echo("An error occured while trying to execute the command", 'error')
print_exc()
finally:
self.add_history(text)
self.clear_input()
def looks_like_command(self, text, name, arguments):
def looks_like_command(self, text, body, name, arguments):
# Command escape stuff ggoes here. If text was prepended by the command
# prefix twice, like //not_a_command (if prefix is set to /) then it
# will be escaped, that is sent just as a regular message with one (only
# one) prefix removed, so message will be /not_a_command.
if name.startswith(self.COMMAND_PREFIX):
self._say_(self, text)
if body.startswith(self.COMMAND_PREFIX):
self.send(body)
return True
def command_preprocessor(self, name, command, arguments, args, kwargs):
def command_preprocessor(self, command, name, arguments, args, kwargs):
# If command argument contain h or help option - forward it to the /help
# command. Dont forget to pass self, as all commands are unbound. And
# also don't forget to print output.
if 'h' in kwargs or 'help' in kwargs:
# Forwarding to the /help command. Dont forget to pass self, as
# all commands are unbound. And also don't forget to print output.
self.echo(self._help_(self, name))
help = self.get_command('help')
self.echo(help(self, name))
return True
def command_postprocessor(self, name, command, arguments, args, kwargs, value):
def command_postprocessor(self, command, name, arguments, args, kwargs, value):
# If command returns a string - print it to a user. A convenient and
# sufficient in most simple cases shortcut to a using echo.
if value and isinstance(value, StringTypes):
self.echo(value)
class CommandTools:
"""
Contains a set of basic tools and shortcuts you can use in your commands to
performe some simple operations.
"""
def echo(self, text, kind='info'):
"""
Print given text to the user.
@ -79,8 +96,8 @@ class ChatMiddleware(CommandProcessor):
"""
Set given text into the input.
"""
message_buffer = self.msg_textview.get_buffer()
message_buffer.set_text(text)
buffer = self.msg_textview.get_buffer()
buffer.set_text(text)
def clear_input(self):
"""
@ -90,8 +107,8 @@ class ChatMiddleware(CommandProcessor):
def add_history(self, text):
"""
Add given text to the input history, so user can scroll through it
using ctrl + up/down arrow keys.
Add given text to the input history, so user can scroll through it using
ctrl + up/down arrow keys.
"""
self.save_sent_message(text)

View File

@ -14,7 +14,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Provides an actual implementation of the standard commands.
Provides an actual implementation for the standard commands.
"""
import dialogs
@ -22,16 +22,19 @@ from common import gajim
from common import helpers
from common.exceptions import GajimGeneralException
from framework import command, CommandError
from middleware import ChatMiddleware
from ..framework import CommandContainer, command
from ..mapping import generate_usage
class CommonCommands(ChatMiddleware):
from hosts import ChatCommands, PrivateChatCommands, GroupChatCommands
class StandardCommonCommands(CommandContainer):
"""
Here defined commands will be common to all, chat, private chat and group
chat. Keep in mind that self is set to an instance of either ChatControl,
PrivateChatControl or GroupchatControl when command is being called.
This command container contains standard commands which are common to all -
chat, private chat, group chat.
"""
HOSTS = (ChatCommands, PrivateChatCommands, GroupChatCommands)
@command
def clear(self):
"""
@ -44,24 +47,30 @@ class CommonCommands(ChatMiddleware):
"""
Hide the chat buttons
"""
self.chat_buttons_set_visible(not self.hide_chat_buttons)
new_status = not self.hide_chat_buttons
self.chat_buttons_set_visible(new_status)
@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
given
Show help on a given command or a list of available commands if -(-a)ll
is given
"""
if command:
command = self.retrieve_command(command)
command = self.get_command(command)
doc = _(command.extract_doc())
usage = command.extract_arg_usage()
documentation = _(command.extract_documentation())
usage = generate_usage(command)
if doc:
return (doc + '\n\n' + usage) if command.usage else doc
else:
return usage
text = str()
if documentation:
text += documentation
if command.usage:
text += ('\n\n' + usage) if text else usage
return text
elif all:
for command in self.list_commands():
names = ', '.join(command.names)
@ -69,7 +78,8 @@ class CommonCommands(ChatMiddleware):
self.echo("%s - %s" % (names, description))
else:
self.echo(self._help_(self, 'help'))
help = self.get_command('help')
self.echo(help(self, 'help'))
@command(raw=True)
def say(self, message):
@ -85,15 +95,12 @@ class CommonCommands(ChatMiddleware):
"""
self.send("/me %s" % action)
class ChatCommands(CommonCommands):
class StandardChatCommands(CommandContainer):
"""
Here defined commands will be unique to a chat. Use it as a hoster to provide
commands which should be unique to a chat. Keep in mind that self is set to
an instance of ChatControl when command is being called.
This command container contains standard command which are unique to a chat.
"""
DISPATCH = True
INHERIT = True
HOSTS = (ChatCommands,)
@command
def ping(self):
@ -104,25 +111,21 @@ class ChatCommands(CommonCommands):
raise CommandError(_('Command is not supported for zeroconf accounts'))
gajim.connections[self.account].sendPing(self.contact)
class PrivateChatCommands(CommonCommands):
class StandardPrivateChatCommands(CommandContainer):
"""
Here defined commands will be unique to a private chat. Use it as a hoster to
provide commands which should be unique to a private chat. Keep in mind that
self is set to an instance of PrivateChatControl when command is being called.
This command container contains standard command which are unique to a
private chat.
"""
DISPATCH = True
INHERIT = True
HOSTS = (PrivateChatCommands,)
class GroupChatCommands(CommonCommands):
class StandardGroupchatCommands(CommandContainer):
"""
Here defined commands will be unique to a group chat. Use it as a hoster to
provide commands which should be unique to a group chat. Keep in mind that
self is set to an instance of GroupchatControl when command is being called.
This command container contains standard command which are unique to a group
chat.
"""
DISPATCH = True
INHERIT = True
HOSTS = (GroupChatCommands,)
@command(raw=True)
def nick(self, new_nick):
@ -192,7 +195,7 @@ class GroupChatCommands(CommonCommands):
gajim.interface.instances[self.account]['join_gc'].window.present()
except KeyError:
try:
dialogs.JoinGroupchatWindow(account=self.account, room_jid=jid, nick=nick)
dialogs.JoinGroupchatWindow(account=None, room_jid=jid, nick=nick)
except GajimGeneralException:
pass
@ -257,6 +260,6 @@ class GroupChatCommands(CommonCommands):
@command(raw=True)
def unblock(self, who):
"""
Allow an occupant to send you public or privates messages
Allow an occupant to send you public or private messages
"""
self.on_unblock(None, who)

View File

@ -0,0 +1,350 @@
# Copyright (C) 2009 red-agent <hell.director@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
The module contains routines to parse command arguments and map them to the
command handler's positonal and keyword arguments.
Mapping is done in two stages: 1) parse arguments into positional arguments and
options; 2) adapt them to the specific command handler according to the command
properties.
"""
import re
from types import BooleanType, UnicodeType
from types import TupleType, ListType
from operator import itemgetter
from errors import DefinitionError, CommandError
# Quite complex piece of regular expression logic to parse options and
# arguments. Might need some tweaking along the way.
ARG_PATTERN = re.compile(r'(\'|")?(?P<body>(?(1).+?|\S+))(?(1)\1)')
OPT_PATTERN = re.compile(r'(?<!\w)--?(?P<key>[\w-]+)(?:(?:=|\s)(\'|")?(?P<value>(?(2)[^-]+?|[^-\s]+))(?(2)\2))?')
# Option keys needs to be encoded to a specific encoding as Python does not
# allow to expand dictionary with raw unicode strings as keys from a **kwargs.
KEY_ENCODING = 'UTF-8'
# Defines how complete representation of command usage (generated based on
# command handler argument specification) will be rendered.
USAGE_PATTERN = 'Usage: %s %s'
def parse_arguments(arguments):
"""
Simple yet effective and sufficient in most cases parser which parses
command arguments and returns them as two lists.
First list represents positional arguments as (argument, position), and
second representing options as (key, value, position) tuples, where position
is a (start, end) span tuple of where it was found in the string.
Options may be given in --long or -short format. As --option=value or
--option value or -option value. Keys without values will get None as value.
Arguments and option values that contain spaces may be given as 'one two
three' or "one two three"; that is between single or double quotes.
"""
args, opts = [], []
def intersects_opts((given_start, given_end)):
"""
Check if given span intersects with any of options.
"""
for key, value, (start, end) in opts:
if given_start >= start and given_end <= end:
return True
return False
def intersects_args((given_start, given_end)):
"""
Check if given span intersects with any of arguments.
"""
for arg, (start, end) in args:
if given_start >= start and given_end <= end:
return True
return False
for match in re.finditer(OPT_PATTERN, arguments):
if match:
key = match.group('key')
value = match.group('value') or None
position = match.span()
opts.append((key, value, position))
for match in re.finditer(ARG_PATTERN, arguments):
if match:
body = match.group('body')
position = match.span()
args.append((body, position))
# Primitive but sufficiently effective way of disposing of conflicted
# sectors. Remove any arguments that intersect with options.
for arg, position in args[:]:
if intersects_opts(position):
args.remove((arg, position))
# Primitive but sufficiently effective way of disposing of conflicted
# sectors. Remove any options that intersect with arguments.
for key, value, position in opts[:]:
if intersects_args(position):
opts.remove((key, value, position))
return args, opts
def adapt_arguments(command, arguments, args, opts):
"""
Adapt args and opts got from the parser to a specific handler by means of
arguments specified on command definition. That is transform them to *args
and **kwargs suitable for passing to a command handler.
Dashes (-) in the option names will be converted to underscores. So you can
map --one-more-option to a one_more_option=None.
If the initial value of a keyword argument is a boolean (False in most
cases) - then this option will be treated as a switch, that is an option
which does not take an argument. If a switch is followed by an argument -
then this argument will be treated just like a normal positional argument.
If the initial value of a keyword argument is a sequence, that is a tuple or
list - then a value of this option will be considered correct only if it is
present in the sequence.
"""
spec_args, spec_kwargs, var_args, var_kwargs = command.extract_specification()
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 in.
if command.raw:
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:
try:
stopper, (start, end) = args[spec_len - 2]
except IndexError:
raise CommandError("Missing arguments", command)
# The essential point of the whole play. After boundaries are
# being determined (supposingly correct) we separate raw part
# from the rest of arguments, which should be normally
# processed.
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)))
else:
# Substitue all of the arguments with only one, which contain
# raw and unprocessed arguments as a string. And discard all the
# options, as raw command does not support them.
args = [(arguments, (0, arguments_end))]
opts = []
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 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):
if key == letter:
expanded.append(letter)
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(norm_kwargs.get(key), BooleanType):
opts[index] = (key, True, position)
if value:
args.append((value, position))
# Sorting arguments and options (just to be sure) in regarding to their
# positions in the string.
args.sort(key=itemgetter(1))
opts.sort(key=itemgetter(2))
# Stripping down position information supplied with arguments and options as
# it won't be needed again.
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:
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 DefinitionError("Can not have both, extra and *args")
# 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 switch and ensure it will not receive any arguments.
# Normally this does not happen unless overlapping is enabled.
for key, value in opts:
initial = norm_kwargs.get(key)
if isinstance(initial, BooleanType):
if not isinstance(value, BooleanType):
raise CommandError("%s: Switch can not take an argument" % key, command)
# Detect every sequence constraint and ensure that if corresponding options
# are given - they contain proper values, within the constraint range.
for key, value in opts:
initial = norm_kwargs.get(key)
if isinstance(initial, (TupleType, ListType)):
if value not in initial:
raise CommandError("%s: Invalid argument" % key, command)
# We need to encode every keyword argument to a simple string, not the
# unicode one, because ** expansion does not support it.
for index, (key, value) in enumerate(opts):
if isinstance(key, UnicodeType):
opts[index] = (key.encode(KEY_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
# handler and being expanded.
return tuple(args), dict(opts)
def generate_usage(command, complete=True):
"""
Extract handler's arguments specification and wrap them in a human-readable
format usage information. If complete is given - then USAGE_PATTERN will be
used to render the specification completly.
"""
spec_args, spec_kwargs, var_args, var_kwargs = command.extract_specification()
# Remove some special positional arguments from the specifiaction, but store
# their names so they can be used for usage info generation.
sp_source = spec_args.pop(0) if command.source else None
sp_extra = spec_args.pop() if command.extra else None
kwargs = []
letters = []
for key, value in spec_kwargs:
letter = key[0]
key = key.replace('_', '-')
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))
letters.append(letter)
else:
kwargs.append('--%s%s' % (key, value))
usage = str()
args = str()
if command.raw:
spec_len = len(spec_args) - 1
if spec_len:
args += ('<%s>' % ', '.join(spec_args[:spec_len])) + ' '
args += ('(|%s|)' if command.empty else '|%s|') % spec_args[-1]
else:
if spec_args:
args += '<%s>' % ', '.join(spec_args)
if var_args or sp_extra:
args += (' ' if spec_args else str()) + '<<%s>>' % (var_args or sp_extra)
usage += args
if kwargs or var_kwargs:
if kwargs:
usage += (' ' if args else str()) + '[%s]' % ', '.join(kwargs)
if var_kwargs:
usage += (' ' if args else str()) + '[[%s]]' % var_kwargs
# Native name will be the first one if it is included. Otherwise, names will
# be in the order they were specified.
if len(command.names) > 1:
names = '%s (%s)' % (command.first_name, ', '.join(command.names[1:]))
else:
names = command.first_name
return USAGE_PATTERN % (names, usage) if complete else usage

View File

@ -1,88 +0,0 @@
# Copyright (C) 2009 red-agent <hell.director@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
This module contains examples of how to create your own commands by creating an
adhoc command processor. Each adhoc command processor should be hosted by one or
more which dispatch the real deal and droppped in to where it belongs.
"""
from framework import command
from implementation import ChatCommands, PrivateChatCommands, GroupChatCommands
class CustomCommonCommands(ChatCommands, PrivateChatCommands, GroupChatCommands):
"""
This adhoc processor will be hosted by a multiple processors which dispatch
commands from all, chat, private chat and group chat. So commands defined
here will be available to all of them.
"""
DISPATCH = True
HOSTED_BY = ChatCommands, PrivateChatCommands, GroupChatCommands
@command
def dance(self):
"""
First line of the doc string is called a description and will be
programmatically extracted.
After that you can give more help, like explanation of the options. This
one will be programatically extracted and formatted too. After this one
there will be autogenerated (based on the method signature) usage
information appended. You can turn it off though, if you want.
"""
return "I can't dance, you stupid fuck, I'm just a command system! A cool one, though..."
class CustomChatCommands(ChatCommands):
"""
This adhoc processor will be hosted by a ChatCommands processor which
dispatches commands from a chat. So commands defined here will be available
only to a chat.
"""
DISPATCH = True
HOSTED_BY = ChatCommands
@command
def sing(self):
return "Are you phreaking kidding me? Buy yourself a damn stereo..."
class CustomPrivateChatCommands(PrivateChatCommands):
"""
This adhoc processor will be hosted by a PrivateChatCommands processor which
dispatches commands from a private chat. So commands defined here will be
available only to a private chat.
"""
DISPATCH = True
HOSTED_BY = PrivateChatCommands
@command
def make_coffee(self):
return "What do I look like, you ass? A coffee machine!?"
class CustomGroupChatCommands(GroupChatCommands):
"""
This adhoc processor will be hosted by a GroupChatCommands processor which
dispatches commands from a group chat. So commands defined here will be
available only to a group chat.
"""
DISPATCH = True
HOSTED_BY = GroupChatCommands
@command
def fetch(self):
return "You should really buy yourself a dog and start torturing it instead of me..."

View File

@ -1,765 +0,0 @@
# Copyright (C) 2009 red-agent <hell.director@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Provides a tiny framework with simple, yet powerful and extensible architecture
to implement commands in a streight and flexible, declarative way.
"""
import re
from types import FunctionType, UnicodeType, TupleType, ListType, BooleanType
from inspect import getargspec
from operator import itemgetter
class InternalError(Exception):
pass
class CommandError(Exception):
def __init__(self, message=None, command=None, name=None):
self.command = command
self.name = name
if command:
self.name = command.first_name
if message:
super(CommandError, self).__init__(message)
else:
super(CommandError, self).__init__()
class Command(object):
DOC_STRIP_PATTERN = re.compile(r'(?:^[ \t]+|\A\n)', re.MULTILINE)
DOC_FORMAT_PATTERN = re.compile(r'(?<!\n)\n(?!\n)', re.MULTILINE)
ARG_USAGE_PATTERN = 'Usage: %s %s'
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
def __call__(self, *args, **kwargs):
try:
return self.handler(*args, **kwargs)
except CommandError, exception:
# Re-raise an excepttion with a proper command attribute set,
# unless it is already set by the one who raised an exception.
if not exception.command and not exception.name:
raise CommandError(exception.message, self)
# Do not forget to re-raise an exception just like it was if at
# least either, command or name attribute is set properly.
raise
# This one is a little bit too wide, but as Python does not have
# anything more constrained - there is no other choice. Take a look here
# if command complains about invalid arguments while they are ok.
except TypeError:
raise CommandError("Command received invalid arguments", self)
def __repr__(self):
return "<Command %s>" % ', '.join(self.names)
def __cmp__(self, other):
"""
Comparison is implemented based on a first name.
"""
return cmp(self.first_name, other.first_name)
@property
def first_name(self):
return self.names[0]
@property
def native_name(self):
return self.handler.__name__
def extract_doc(self):
"""
Extract handler's doc-string and transform it to a usable format.
"""
doc = self.handler.__doc__ or None
if not doc:
return
doc = re.sub(self.DOC_STRIP_PATTERN, str(), doc)
doc = re.sub(self.DOC_FORMAT_PATTERN, ' ', doc)
return doc
def extract_description(self):
"""
Extract handler's description (which is a first line of the doc). Try to
keep them simple yet meaningful.
"""
doc = self.extract_doc()
return doc.split('\n', 1)[0] if doc else None
def extract_arg_spec(self):
names, var_args, var_kwargs, defaults = getargspec(self.handler)
# Behavior of this code need to be checked. Might yield incorrect
# results on some rare occasions.
spec_args = names[:-len(defaults) if defaults else len(names)]
spec_kwargs = list(zip(names[-len(defaults):], defaults)) if defaults else {}
# Removing self from arguments specification. Command handler should
# normally be an instance method.
if spec_args.pop(0) != 'self':
raise InternalError("First argument must be self")
return spec_args, spec_kwargs, var_args, var_kwargs
def extract_arg_usage(self, complete=True):
"""
Extract handler's arguments specification and wrap them in a
human-readable format. If complete is given - then ARG_USAGE_PATTERN
will be used to render it completly.
"""
spec_args, spec_kwargs, var_args, var_kwargs = self.extract_arg_spec()
# Remove some special positional arguments from the specifiaction, but
# store their names so they can be used for usage info generation.
sp_source = spec_args.pop(0) if self.source else None
sp_extra = spec_args.pop() if self.extra else None
kwargs = []
letters = []
for key, value in spec_kwargs:
letter = key[0]
key = key.replace('_', '-')
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))
letters.append(letter)
else:
kwargs.append('--%s%s' % (key, value))
usage = str()
args = str()
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:
args += (' ' if spec_args else str()) + '<<%s>>' % (var_args or sp_extra)
usage += args
if kwargs or var_kwargs:
if kwargs:
usage += (' ' if args else str()) + '[%s]' % ', '.join(kwargs)
if var_kwargs:
usage += (' ' if args else str()) + '[[%s]]' % var_kwargs
# Native name will be the first one if it is included. Otherwise, names
# will be in the order they were specified.
if len(self.names) > 1:
names = '%s (%s)' % (self.first_name, ', '.join(self.names[1:]))
else:
names = self.first_name
return usage if not complete else self.ARG_USAGE_PATTERN % (names, usage)
class Dispatcher(type):
table = {}
hosted = {}
def __init__(cls, name, 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)
# Sanitize names even if processor is not suitable for registering,
# because it might be inherited by an another processor.
Dispatcher.sanitize_names(cls)
super(Dispatcher, cls).__init__(name, bases, dct)
@classmethod
def is_suitable(cls, proc, dct):
is_not_root = dct.get('__metaclass__') is not cls
to_be_dispatched = bool(dct.get('DISPATCH'))
return is_not_root and to_be_dispatched
@classmethod
def check_if_dispatchable(cls, bases, dct):
dispatcher = dct.get('DISPATCHED_BY')
if not dispatcher:
return False
if dispatcher not in bases:
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 False
if not isinstance(hosters, (TupleType, ListType)):
hosters = (hosters,)
for hoster in hosters:
if hoster not in bases:
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):
cls.table[proc] = {}
inherit = proc.__dict__.get('INHERIT')
if 'HOSTED_BY' in proc.__dict__:
cls.register_adhocs(proc)
commands = cls.traverse_commands(proc, inherit)
cls.register_commands(proc, commands)
@classmethod
def sanitize_names(cls, proc):
inherit = proc.__dict__.get('INHERIT')
commands = cls.traverse_commands(proc, inherit)
for key, command in commands:
if not proc.SAFE_NAME_SCAN_PATTERN.match(key):
setattr(proc, proc.SAFE_NAME_SUBS_PATTERN % key, command)
try:
delattr(proc, key)
except AttributeError:
pass
@classmethod
def traverse_commands(cls, proc, inherit=True):
keys = dir(proc) if inherit else proc.__dict__.iterkeys()
for key in keys:
value = getattr(proc, key)
if isinstance(value, Command):
yield key, value
@classmethod
def register_commands(cls, proc, commands):
for key, command in commands:
for name in command.names:
name = proc.prepare_name(name)
if name not in cls.table[proc]:
cls.table[proc][name] = command
else:
raise InternalError("Command with name %s already exists" % name)
@classmethod
def register_adhocs(cls, proc):
hosters = proc.HOSTED_BY
if not isinstance(hosters, (TupleType, ListType)):
hosters = (hosters,)
for hoster in hosters:
if hoster in cls.hosted:
cls.hosted[hoster].append(proc)
else:
cls.hosted[hoster] = [proc]
@classmethod
def retrieve_command(cls, proc, name):
command = cls.table[proc.DISPATCHED_BY].get(name)
if command:
return command
if proc.DISPATCHED_BY in cls.hosted:
for adhoc in cls.hosted[proc.DISPATCHED_BY]:
command = cls.table[adhoc].get(name)
if command:
return command
@classmethod
def list_commands(cls, proc):
commands = dict(cls.traverse_commands(proc.DISPATCHED_BY))
if proc.DISPATCHED_BY in cls.hosted:
for adhoc in cls.hosted[proc.DISPATCHED_BY]:
inherit = adhoc.__dict__.get('INHERIT')
commands.update(dict(cls.traverse_commands(adhoc, inherit)))
return commands.values()
class CommandProcessor(object):
"""
A base class for a drop-in command processor which you can drop (make your
class to inherit from it) in any of your classes to support commands. In
order to get it done you need to make your own processor, inheriter from
CommandProcessor and then drop it in. Don't forget about few important steps
described below.
Every command in the processor (normally) will gain full access through self
to an object you are adding commands to.
Your subclass, which will contain commands should define in its body
DISPATCH = True in order to be included in the dispatching table.
Every class you will drop the processor in should define DISPATCHED_BY set
to the same processor you are inheriting from.
Names of the commands after preparation stuff id done will be sanitized
(based on SAFE_NAME_SCAN_PATTERN and SAFE_NAME_SUBS_PATTERN) in order not to
interfere with the methods defined in a class you will drop a processor in.
If you want to create an adhoc processor (then one that parasites on the
other one (the host), so it does not have to be included directly into
whatever includes the host) you need to inherit you processor from the host
and set HOSTED_BY to that host.
INHERIT controls whether commands inherited from base classes (which could
include other processors) will be registered or not. This is disabled
by-default because it leads to unpredictable consequences when used in adhoc
processors which inherit from more then one processor or has such processors
in its inheritance tree. In that case - encapsulation is being broken and
some (all) commands are shared between non-related processors.
"""
__metaclass__ = Dispatcher
SAFE_NAME_SCAN_PATTERN = re.compile(r'_(?P<name>\w+)_')
SAFE_NAME_SUBS_PATTERN = '_%s_'
# Quite complex piece of regular expression logic.
ARG_PATTERN = re.compile(r'(\'|")?(?P<body>(?(1).+?|\S+))(?(1)\1)')
OPT_PATTERN = re.compile(r'(?<!\w)--?(?P<key>[\w-]+)(?:(?:=|\s)(\'|")?(?P<value>(?(2)[^-]+?|[^-\s]+))(?(2)\2))?')
COMMAND_PREFIX = '/'
CASE_SENSITIVE_COMMANDS = False
ARG_ENCODING = 'utf8'
def __getattr__(self, name):
"""
This allows to reach and directly (internally) call commands which are
defined in (other) adhoc processors.
"""
command_name = self.SAFE_NAME_SCAN_PATTERN.match(name)
if command_name:
command = self.retrieve_command(command_name.group('name'))
if command:
return command
raise AttributeError(name)
@classmethod
def prepare_name(cls, name):
return name if cls.CASE_SENSITIVE_COMMANDS else name.lower()
@classmethod
def retrieve_command(cls, name):
name = cls.prepare_name(name)
command = Dispatcher.retrieve_command(cls, name)
if not command:
raise CommandError("Command does not exist", name=name)
return command
@classmethod
def list_commands(cls):
commands = Dispatcher.list_commands(cls)
return sorted(set(commands))
@classmethod
def parse_command_arguments(cls, arguments):
"""
Simple yet effective and sufficient in most cases parser which parses
command arguments and returns them as two lists. First represents
positional arguments as (argument, position), and second representing
options as (key, value, position) tuples, where position is a (start,
end) span tuple of where it was found in the string.
The format of the input arguments should be:
<arg1, arg2> <<extra>> [-(-o)ption=value1, -(-a)nother=value2] [[extra_options]]
Options may be given in --long or -short format. As --option=value or
--option value or -option value. Keys without values will get True as
value. Arguments and option values that contain spaces may be given as
'one two three' or "one two three"; that is between single or double
quotes.
"""
args, opts = [], []
def intersects_opts((given_start, given_end)):
"""
Check if something intersects with boundaries of any parsed option.
"""
for key, value, (start, end) in opts:
if given_start >= start and given_end <= end:
return True
return False
def intersects_args((given_start, given_end)):
"""
Check if something intersects with boundaries of any parsed argument.
"""
for arg, (start, end) in args:
if given_start >= start and given_end <= end:
return True
return False
for match in re.finditer(cls.OPT_PATTERN, arguments):
if match:
key = match.group('key')
value = match.group('value') or None
position = match.span()
opts.append((key, value, position))
for match in re.finditer(cls.ARG_PATTERN, arguments):
if match and not intersects_opts(match.span()):
body = match.group('body')
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
def adapt_command_arguments(cls, command, arguments, args, opts):
"""
Adapts args and opts got from the parser to a specific handler by means
of arguments specified on command definition. That is transforms them to
*args and **kwargs suitable for passing to a command handler.
Extra arguments which are not considered extra (or optional) - will be
passed as if they were value for keywords, in the order keywords are
defined and printed in usage.
Dashes (-) in the option names will be converted to underscores. So you
can map --one-more-option to a one_more_option=None.
If initial value of a keyword argument is a boolean (False in most
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()
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 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:
try:
stopper, (start, end) = args[spec_len - 2]
except IndexError:
raise CommandError("Missing arguments", command)
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))]
opts = []
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 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):
if key == letter:
expanded.append(letter)
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(norm_kwargs.get(key), BooleanType):
opts[index] = (key, True, position)
if value:
args.append((value, position))
# Sorting arguments and options (just to be sure) in regarding to their
# positions in the string.
args.sort(key=itemgetter(1))
opts.sort(key=itemgetter(2))
# Stripping down position information supplied with arguments and options as it
# won't be needed again.
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:
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 InternalError("Can not have both, extra and *args")
# 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:
raise CommandError("Wrong argument", command)
# Detect every switch and ensure it will not receive any arguments.
# Normally this does not happen unless overlapping is enabled.
for key, value in opts:
initial = norm_kwargs.get(key)
if isinstance(initial, BooleanType) and not isinstance(value, BooleanType):
raise CommandError("Switches do not take arguments", command)
# We need to encode every keyword argument to a simple string, not the
# unicode one, because ** expansion does not support it.
for index, (key, value) in enumerate(opts):
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):
"""
Try to process text as a command. Returns True if it is a command and
False if it is not.
"""
if not text.startswith(self.COMMAND_PREFIX):
return False
body = text[len(self.COMMAND_PREFIX):]
body = body.strip()
parts = body.split(' ', 1)
name, arguments = parts if len(parts) > 1 else (parts[0], None)
flag = self.looks_like_command(body, name, arguments)
if flag is not None:
return flag
self.execute_command(text, name, arguments)
return True
def execute_command(self, text, name, arguments):
command = self.retrieve_command(name)
args, opts = self.parse_command_arguments(arguments) if arguments else ([], [])
args, kwargs = self.adapt_command_arguments(command, arguments, args, opts)
if self.command_preprocessor(name, command, arguments, args, kwargs):
return
value = command(self, *args, **kwargs)
self.command_postprocessor(name, command, arguments, args, kwargs, value)
def command_preprocessor(self, name, command, arguments, args, kwargs):
"""
Redefine this method in the subclass to execute custom code before
command gets executed. If returns True then command execution will be
interrupted and command will not be executed.
"""
pass
def command_postprocessor(self, name, command, arguments, args, kwargs, output):
"""
Redefine this method in the subclass to execute custom code after
command gets executed.
"""
pass
def looks_like_command(self, text, name, arguments):
"""
This hook is being called before any processing, but after it was
determined that text looks like a command. If returns non None value
- then further processing will be interrupted and that value will be
used to return from process_as_command.
"""
pass
def command(*names, **kwargs):
"""
A decorator which provides a declarative way of defining commands.
You can specify a set of names by which you can call the command. If names
is empty - then the name of the command will be set to native one (extracted
from the handler name).
If include_native=True argument is given and names is non-empty - then
native name will be added as well.
If usage=True is given - then handler's doc will be appended with an
auto-generated usage info.
If source=True is given - then the first positional argument of the command
handler will receive a string with a raw and unprocessed source arguments.
If raw=True is given - then command should define only one argument to
which all raw and unprocessed source arguments will be given.
If empty=True is given - then when raw=True is set and command receives no
arguments - an exception will be raised.
If extra=True is given - then last positional argument will receive every
extra positional argument that will be given to a command. This is an
analogue to specifing *args, but the latter one should be used in simplest
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
arguments. Expansion is made on a first-letter comparison basis. If more
then one long option with the same first letter defined - only first one
will be used in expansion.
"""
names = list(names)
include_native = kwargs.get('include_native', True)
usage = kwargs.get('usage', True)
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, overlap, empty, expand_short)
# Extract and inject native name while making sure it is going to be the
# first one in the list.
if not names or include_native:
names.insert(0, command.native_name)
command.names = tuple(names)
return command
# Workaround if we are getting called without parameters. Keep in mind that
# in that case - first item in the names will be the handler.
if len(names) == 1 and isinstance(names[0], FunctionType):
return decorator(names.pop())
return decorator

View File

@ -231,14 +231,6 @@ from chat_control import ChatControl
from groupchat_control import GroupchatControl
from groupchat_control import PrivateChatControl
# Here custom adhoc processors should be loaded. At this point there is
# everything they need to function properly. The next line loads custom exmple
# adhoc processors. Technically, they could be loaded earlier as host processors
# themself does not depend on the chat controls, but that should not be done
# uless there is a really good reason for that..
#
# from commands import custom
from atom_window import AtomWindow
from session import ChatControlSession

View File

@ -47,7 +47,8 @@ from chat_control import ChatControl
from chat_control import ChatControlBase
from common.exceptions import GajimGeneralException
from commands.implementation import PrivateChatCommands, GroupChatCommands
from command_system.implementation.hosts import PrivateChatCommands
from command_system.implementation.hosts import GroupChatCommands
import logging
log = logging.getLogger('gajim.groupchat_control')
@ -118,10 +119,12 @@ def tree_cell_data_func(column, renderer, model, iter_, tv=None):
renderer.set_property('font',
gtkgui_helpers.get_theme_font_for_option(theme, 'groupfont'))
class PrivateChatControl(ChatControl, PrivateChatCommands):
class PrivateChatControl(ChatControl):
TYPE_ID = message_control.TYPE_PM
DISPATCHED_BY = PrivateChatCommands
# Set a command host to bound to. Every command given through a private chat
# will be processed with this command host.
COMMAND_HOST = PrivateChatCommands
def __init__(self, parent_win, gc_contact, contact, account, session):
room_jid = contact.jid.split('/')[0]
@ -185,10 +188,12 @@ class PrivateChatControl(ChatControl, PrivateChatCommands):
self.session.negotiate_e2e(False)
class GroupchatControl(ChatControlBase, GroupChatCommands):
class GroupchatControl(ChatControlBase):
TYPE_ID = message_control.TYPE_GC
DISPATCHED_BY = GroupChatCommands
# Set a command host to bound to. Every command given through a group chat
# will be processed with this command host.
COMMAND_HOST = GroupChatCommands
def __init__(self, parent_win, contact, acct, is_continued=False):
ChatControlBase.__init__(self, self.TYPE_ID, parent_win,