337 lines
12 KiB
Python
337 lines
12 KiB
Python
# 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) and len(text) > 1):
|
|
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(error.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
|
|
|
|
def documentation(text):
|
|
"""
|
|
This decorator is used to bind a documentation (a help) to a command.
|
|
|
|
Though this can be done easily by using doc-strings in a declarative and
|
|
Pythonic way - some of Gajim's developers are against it because of the
|
|
scaffolding needed to support the tranlation of such documentation.
|
|
"""
|
|
def decorator(target):
|
|
if isinstance(target, Command):
|
|
target.handler.__doc__ = text
|
|
else:
|
|
target.__doc__ = text
|
|
return target
|
|
|
|
return decorator
|