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:
parent
ca127b8d31
commit
958d937d5c
15 changed files with 1055 additions and 937 deletions
|
@ -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_XHTML, NS_XHTML_IM, NS_FILE, NS_MUC
|
||||||
from common.xmpp.protocol import NS_RECEIPTS, NS_ESESSION
|
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:
|
try:
|
||||||
import gtkspell
|
import gtkspell
|
||||||
|
@ -81,7 +87,7 @@ if gajim.config.get('use_speller') and HAS_GTK_SPELL:
|
||||||
del tv
|
del tv
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
class ChatControlBase(MessageControl, CommonCommands):
|
class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
|
||||||
'''A base class containing a banner, ConversationTextview, MessageTextView
|
'''A base class containing a banner, ConversationTextview, MessageTextView
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@ -1164,12 +1170,14 @@ class ChatControlBase(MessageControl, CommonCommands):
|
||||||
# FIXME: Set sensitivity for toolbar
|
# FIXME: Set sensitivity for toolbar
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
class ChatControl(ChatControlBase, ChatCommands):
|
class ChatControl(ChatControlBase):
|
||||||
'''A control for standard 1-1 chat'''
|
'''A control for standard 1-1 chat'''
|
||||||
TYPE_ID = message_control.TYPE_CHAT
|
TYPE_ID = message_control.TYPE_CHAT
|
||||||
old_msg_kind = None # last kind of the printed message
|
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):
|
def __init__(self, parent_win, contact, acct, session, resource = None):
|
||||||
ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
|
ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
|
||||||
|
|
20
src/command_system/__init__.py
Normal file
20
src/command_system/__init__.py
Normal 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.
|
||||||
|
"""
|
89
src/command_system/dispatching.py
Normal file
89
src/command_system/dispatching.py
Normal 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)
|
41
src/command_system/errors.py
Normal file
41
src/command_system/errors.py
Normal 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
|
320
src/command_system/framework.py
Normal file
320
src/command_system/framework.py
Normal 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
|
|
@ -14,7 +14,6 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
The command system providing scalable and convenient architecture in combination
|
The implementation and auxilary systems which implement the standard Gajim
|
||||||
with declarative way of defining commands and a fair amount of automatization
|
commands and also provide an infrastructure for adding custom commands.
|
||||||
for routine processes.
|
|
||||||
"""
|
"""
|
85
src/command_system/implementation/custom.py
Normal file
85
src/command_system/implementation/custom.py
Normal 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..."
|
42
src/command_system/implementation/hosts.py
Normal file
42
src/command_system/implementation/hosts.py
Normal 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
|
|
@ -16,53 +16,70 @@
|
||||||
"""
|
"""
|
||||||
Provides a glue to tie command system framework and the actual code where it
|
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
|
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
|
between the two and a few utility methods so you don't need to dig up the code
|
||||||
code to write basic commands.
|
itself code to write basic commands.
|
||||||
"""
|
"""
|
||||||
from common import gajim
|
|
||||||
|
|
||||||
from types import StringTypes
|
from types import StringTypes
|
||||||
from framework import CommandProcessor, CommandError
|
|
||||||
from traceback import print_exc
|
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.
|
A basic scaffolding to provide convenient interaction between the command
|
||||||
Also provides some few basic utilities for the same purpose.
|
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:
|
try:
|
||||||
super(ChatMiddleware, self).execute_command(text, name, arguments)
|
super(ChatCommandProcessor, self).execute_command(name, arguments)
|
||||||
except CommandError, exception:
|
except CommandError, error:
|
||||||
self.echo("%s: %s" %(exception.name, exception.message), 'error')
|
self.echo("%s: %s" %(error.name, error.message), 'error')
|
||||||
except Exception:
|
except Exception:
|
||||||
self.echo("An error occured while trying to execute the command", 'error')
|
self.echo("An error occured while trying to execute the command", 'error')
|
||||||
print_exc()
|
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
|
# 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
|
# 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
|
# will be escaped, that is sent just as a regular message with one (only
|
||||||
# one) prefix removed, so message will be /not_a_command.
|
# one) prefix removed, so message will be /not_a_command.
|
||||||
if name.startswith(self.COMMAND_PREFIX):
|
if body.startswith(self.COMMAND_PREFIX):
|
||||||
self._say_(self, text)
|
self.send(body)
|
||||||
return True
|
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:
|
if 'h' in kwargs or 'help' in kwargs:
|
||||||
# Forwarding to the /help command. Dont forget to pass self, as
|
help = self.get_command('help')
|
||||||
# all commands are unbound. And also don't forget to print output.
|
self.echo(help(self, name))
|
||||||
self.echo(self._help_(self, name))
|
|
||||||
return True
|
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):
|
if value and isinstance(value, StringTypes):
|
||||||
self.echo(value)
|
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'):
|
def echo(self, text, kind='info'):
|
||||||
"""
|
"""
|
||||||
Print given text to the user.
|
Print given text to the user.
|
||||||
|
@ -79,8 +96,8 @@ class ChatMiddleware(CommandProcessor):
|
||||||
"""
|
"""
|
||||||
Set given text into the input.
|
Set given text into the input.
|
||||||
"""
|
"""
|
||||||
message_buffer = self.msg_textview.get_buffer()
|
buffer = self.msg_textview.get_buffer()
|
||||||
message_buffer.set_text(text)
|
buffer.set_text(text)
|
||||||
|
|
||||||
def clear_input(self):
|
def clear_input(self):
|
||||||
"""
|
"""
|
||||||
|
@ -90,8 +107,8 @@ class ChatMiddleware(CommandProcessor):
|
||||||
|
|
||||||
def add_history(self, text):
|
def add_history(self, text):
|
||||||
"""
|
"""
|
||||||
Add given text to the input history, so user can scroll through it
|
Add given text to the input history, so user can scroll through it using
|
||||||
using ctrl + up/down arrow keys.
|
ctrl + up/down arrow keys.
|
||||||
"""
|
"""
|
||||||
self.save_sent_message(text)
|
self.save_sent_message(text)
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# 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
|
import dialogs
|
||||||
|
@ -22,16 +22,19 @@ from common import gajim
|
||||||
from common import helpers
|
from common import helpers
|
||||||
from common.exceptions import GajimGeneralException
|
from common.exceptions import GajimGeneralException
|
||||||
|
|
||||||
from framework import command, CommandError
|
from ..framework import CommandContainer, command
|
||||||
from middleware import ChatMiddleware
|
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
|
This command container contains standard commands which are common to all -
|
||||||
chat. Keep in mind that self is set to an instance of either ChatControl,
|
chat, private chat, group chat.
|
||||||
PrivateChatControl or GroupchatControl when command is being called.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
HOSTS = (ChatCommands, PrivateChatCommands, GroupChatCommands)
|
||||||
|
|
||||||
@command
|
@command
|
||||||
def clear(self):
|
def clear(self):
|
||||||
"""
|
"""
|
||||||
|
@ -44,24 +47,30 @@ class CommonCommands(ChatMiddleware):
|
||||||
"""
|
"""
|
||||||
Hide the chat buttons
|
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)
|
@command(overlap=True)
|
||||||
def help(self, command=None, all=False):
|
def help(self, command=None, all=False):
|
||||||
"""
|
"""
|
||||||
Show help on a given command or a list of available commands if -(-a)ll is
|
Show help on a given command or a list of available commands if -(-a)ll
|
||||||
given
|
is given
|
||||||
"""
|
"""
|
||||||
if command:
|
if command:
|
||||||
command = self.retrieve_command(command)
|
command = self.get_command(command)
|
||||||
|
|
||||||
doc = _(command.extract_doc())
|
documentation = _(command.extract_documentation())
|
||||||
usage = command.extract_arg_usage()
|
usage = generate_usage(command)
|
||||||
|
|
||||||
if doc:
|
text = str()
|
||||||
return (doc + '\n\n' + usage) if command.usage else doc
|
|
||||||
else:
|
if documentation:
|
||||||
return usage
|
text += documentation
|
||||||
|
|
||||||
|
if command.usage:
|
||||||
|
text += ('\n\n' + usage) if text else usage
|
||||||
|
|
||||||
|
return text
|
||||||
elif all:
|
elif all:
|
||||||
for command in self.list_commands():
|
for command in self.list_commands():
|
||||||
names = ', '.join(command.names)
|
names = ', '.join(command.names)
|
||||||
|
@ -69,7 +78,8 @@ class CommonCommands(ChatMiddleware):
|
||||||
|
|
||||||
self.echo("%s - %s" % (names, description))
|
self.echo("%s - %s" % (names, description))
|
||||||
else:
|
else:
|
||||||
self.echo(self._help_(self, 'help'))
|
help = self.get_command('help')
|
||||||
|
self.echo(help(self, 'help'))
|
||||||
|
|
||||||
@command(raw=True)
|
@command(raw=True)
|
||||||
def say(self, message):
|
def say(self, message):
|
||||||
|
@ -85,15 +95,12 @@ class CommonCommands(ChatMiddleware):
|
||||||
"""
|
"""
|
||||||
self.send("/me %s" % action)
|
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
|
This command container contains standard command which are unique to a chat.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DISPATCH = True
|
HOSTS = (ChatCommands,)
|
||||||
INHERIT = True
|
|
||||||
|
|
||||||
@command
|
@command
|
||||||
def ping(self):
|
def ping(self):
|
||||||
|
@ -104,25 +111,21 @@ class ChatCommands(CommonCommands):
|
||||||
raise CommandError(_('Command is not supported for zeroconf accounts'))
|
raise CommandError(_('Command is not supported for zeroconf accounts'))
|
||||||
gajim.connections[self.account].sendPing(self.contact)
|
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
|
This command container contains standard command which are unique to a
|
||||||
provide commands which should be unique to a private chat. Keep in mind that
|
private chat.
|
||||||
self is set to an instance of PrivateChatControl when command is being called.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DISPATCH = True
|
HOSTS = (PrivateChatCommands,)
|
||||||
INHERIT = True
|
|
||||||
|
|
||||||
class GroupChatCommands(CommonCommands):
|
class StandardGroupchatCommands(CommandContainer):
|
||||||
"""
|
"""
|
||||||
Here defined commands will be unique to a group chat. Use it as a hoster to
|
This command container contains standard command which are unique to a group
|
||||||
provide commands which should be unique to a group chat. Keep in mind that
|
chat.
|
||||||
self is set to an instance of GroupchatControl when command is being called.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DISPATCH = True
|
HOSTS = (GroupChatCommands,)
|
||||||
INHERIT = True
|
|
||||||
|
|
||||||
@command(raw=True)
|
@command(raw=True)
|
||||||
def nick(self, new_nick):
|
def nick(self, new_nick):
|
||||||
|
@ -192,7 +195,7 @@ class GroupChatCommands(CommonCommands):
|
||||||
gajim.interface.instances[self.account]['join_gc'].window.present()
|
gajim.interface.instances[self.account]['join_gc'].window.present()
|
||||||
except KeyError:
|
except KeyError:
|
||||||
try:
|
try:
|
||||||
dialogs.JoinGroupchatWindow(account=self.account, room_jid=jid, nick=nick)
|
dialogs.JoinGroupchatWindow(account=None, room_jid=jid, nick=nick)
|
||||||
except GajimGeneralException:
|
except GajimGeneralException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -257,6 +260,6 @@ class GroupChatCommands(CommonCommands):
|
||||||
@command(raw=True)
|
@command(raw=True)
|
||||||
def unblock(self, who):
|
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)
|
self.on_unblock(None, who)
|
350
src/command_system/mapping.py
Normal file
350
src/command_system/mapping.py
Normal 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
|
|
@ -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..."
|
|
|
@ -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
|
|
|
@ -231,14 +231,6 @@ from chat_control import ChatControl
|
||||||
from groupchat_control import GroupchatControl
|
from groupchat_control import GroupchatControl
|
||||||
from groupchat_control import PrivateChatControl
|
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 atom_window import AtomWindow
|
||||||
from session import ChatControlSession
|
from session import ChatControlSession
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,8 @@ from chat_control import ChatControl
|
||||||
from chat_control import ChatControlBase
|
from chat_control import ChatControlBase
|
||||||
from common.exceptions import GajimGeneralException
|
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
|
import logging
|
||||||
log = logging.getLogger('gajim.groupchat_control')
|
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',
|
renderer.set_property('font',
|
||||||
gtkgui_helpers.get_theme_font_for_option(theme, 'groupfont'))
|
gtkgui_helpers.get_theme_font_for_option(theme, 'groupfont'))
|
||||||
|
|
||||||
class PrivateChatControl(ChatControl, PrivateChatCommands):
|
class PrivateChatControl(ChatControl):
|
||||||
TYPE_ID = message_control.TYPE_PM
|
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):
|
def __init__(self, parent_win, gc_contact, contact, account, session):
|
||||||
room_jid = contact.jid.split('/')[0]
|
room_jid = contact.jid.split('/')[0]
|
||||||
|
@ -185,10 +188,12 @@ class PrivateChatControl(ChatControl, PrivateChatCommands):
|
||||||
|
|
||||||
self.session.negotiate_e2e(False)
|
self.session.negotiate_e2e(False)
|
||||||
|
|
||||||
class GroupchatControl(ChatControlBase, GroupChatCommands):
|
class GroupchatControl(ChatControlBase):
|
||||||
TYPE_ID = message_control.TYPE_GC
|
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):
|
def __init__(self, parent_win, contact, acct, is_continued=False):
|
||||||
ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
|
ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
|
||||||
|
|
Loading…
Add table
Reference in a new issue