9578053 Jan 22 2022 distfiles.gentoo.org/distfiles/gajim-1.3.3-2.tar.gz
This commit is contained in:
parent
a5b3822651
commit
4c1b226bff
1045 changed files with 753037 additions and 18 deletions
20
gajim/command_system/__init__.py
Normal file
20
gajim/command_system/__init__.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@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.
|
||||
"""
|
135
gajim/command_system/dispatcher.py
Normal file
135
gajim/command_system/dispatcher.py
Normal file
|
@ -0,0 +1,135 @@
|
|||
# Copyright (c) 2010, Alexander Cherniuk (ts33kr@gmail.com)
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
#
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
Backbone of the command system. Provides smart and controllable
|
||||
dispatching mechanism with an auto-discovery functionality. In addition
|
||||
to automatic discovery and dispatching, also features manual control
|
||||
over the process.
|
||||
"""
|
||||
|
||||
from typing import Any # pylint: disable=unused-import
|
||||
from typing import Dict # pylint: disable=unused-import
|
||||
|
||||
from gajim.command_system.tools import remove
|
||||
|
||||
COMMANDS = {} # type: Dict[Any, Any]
|
||||
CONTAINERS = {} # type: Dict[Any, Any]
|
||||
|
||||
|
||||
def add_host(host):
|
||||
CONTAINERS[host] = []
|
||||
|
||||
|
||||
def remove_host(host):
|
||||
remove(CONTAINERS, host)
|
||||
|
||||
|
||||
def add_container(container):
|
||||
for host in container.HOSTS:
|
||||
CONTAINERS[host].append(container)
|
||||
|
||||
|
||||
def remove_container(container):
|
||||
for host in container.HOSTS:
|
||||
remove(CONTAINERS[host], container)
|
||||
|
||||
|
||||
def add_commands(container):
|
||||
commands = COMMANDS.setdefault(container, {})
|
||||
for command in traverse_commands(container):
|
||||
for name in command.names:
|
||||
commands[name] = command
|
||||
|
||||
|
||||
def remove_commands(container):
|
||||
remove(COMMANDS, container)
|
||||
|
||||
|
||||
def traverse_commands(container):
|
||||
for name in dir(container):
|
||||
attribute = getattr(container, name)
|
||||
if is_command(attribute):
|
||||
yield attribute
|
||||
|
||||
|
||||
def is_command(attribute):
|
||||
from gajim.command_system.framework import Command
|
||||
return isinstance(attribute, Command)
|
||||
|
||||
|
||||
def is_root(namespace):
|
||||
metaclass = namespace.get("__metaclass__", None)
|
||||
if not metaclass:
|
||||
return False
|
||||
return issubclass(metaclass, Dispatchable)
|
||||
|
||||
|
||||
def get_command(host, name):
|
||||
for container in CONTAINERS[host]:
|
||||
command = COMMANDS[container].get(name)
|
||||
if command:
|
||||
return command
|
||||
|
||||
|
||||
def list_commands(host):
|
||||
for container in CONTAINERS[host]:
|
||||
commands = COMMANDS[container]
|
||||
for name, command in commands.items():
|
||||
yield name, command
|
||||
|
||||
|
||||
class Dispatchable(type):
|
||||
# pylint: disable=no-value-for-parameter
|
||||
def __init__(cls, name, bases, namespace):
|
||||
parents = super(Dispatchable, cls)
|
||||
parents.__init__(name, bases, namespace)
|
||||
if not is_root(namespace):
|
||||
cls.dispatch()
|
||||
|
||||
def dispatch(cls):
|
||||
if cls.AUTOMATIC:
|
||||
cls.enable()
|
||||
|
||||
|
||||
class Host(Dispatchable):
|
||||
|
||||
def enable(cls):
|
||||
add_host(cls)
|
||||
|
||||
def disable(cls):
|
||||
remove_host(cls)
|
||||
|
||||
|
||||
class Container(Dispatchable):
|
||||
|
||||
def enable(cls):
|
||||
add_container(cls)
|
||||
add_commands(cls)
|
||||
|
||||
def disable(cls):
|
||||
remove_commands(cls)
|
||||
remove_container(cls)
|
54
gajim/command_system/errors.py
Normal file
54
gajim/command_system/errors.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@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.message = message
|
||||
|
||||
self.command = command
|
||||
self.name = name
|
||||
|
||||
if command and not name:
|
||||
self.name = command.first_name
|
||||
|
||||
super(BaseError, self).__init__()
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
|
||||
class DefinitionError(BaseError):
|
||||
"""
|
||||
Used to indicate errors occurred on command definition.
|
||||
"""
|
||||
|
||||
|
||||
class CommandError(BaseError):
|
||||
"""
|
||||
Used to indicate errors occurred during command execution.
|
||||
"""
|
||||
|
||||
|
||||
class NoCommandError(BaseError):
|
||||
"""
|
||||
Used to indicate an inability to find the specified command.
|
||||
"""
|
351
gajim/command_system/framework.py
Normal file
351
gajim/command_system/framework.py
Normal file
|
@ -0,0 +1,351 @@
|
|||
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@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 straight and flexible,
|
||||
declarative way.
|
||||
"""
|
||||
|
||||
from types import FunctionType
|
||||
from inspect import getargspec, getdoc
|
||||
|
||||
from gajim.command_system.dispatcher import Host
|
||||
from gajim.command_system.dispatcher import Container
|
||||
from gajim.command_system.dispatcher import get_command
|
||||
from gajim.command_system.dispatcher import list_commands
|
||||
from gajim.command_system.mapping import parse_arguments
|
||||
from gajim.command_system.mapping import adapt_arguments
|
||||
from gajim.command_system.errors import DefinitionError
|
||||
from gajim.command_system.errors import CommandError
|
||||
from gajim.command_system.errors import NoCommandError
|
||||
|
||||
|
||||
class CommandHost(metaclass=Host):
|
||||
"""
|
||||
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.
|
||||
|
||||
The AUTOMATIC class variable, which must be defined by a command
|
||||
host, specifies whether the command host should be automatically
|
||||
dispatched and enabled by the dispatcher or not.
|
||||
"""
|
||||
__metaclass__ = Host
|
||||
|
||||
|
||||
class CommandContainer(metaclass=Container):
|
||||
"""
|
||||
Command container is an entity which holds defined commands,
|
||||
allowing them to be dispatched and processed correctly. Each
|
||||
command container may be bound to a one or more command hosts.
|
||||
|
||||
The AUTOMATIC class variable, which must be defined by a command
|
||||
processor, specifies whether the command processor should be
|
||||
automatically dispatched and enabled by the dispatcher or not.
|
||||
|
||||
Bounding is controlled by the HOSTS class variable, which must be
|
||||
defined by the command container. This variable should contain a
|
||||
sequence of hosts to bound to, as a tuple or list.
|
||||
"""
|
||||
__metaclass__ = Container
|
||||
|
||||
|
||||
class CommandProcessor:
|
||||
"""
|
||||
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
|
||||
# precede a text in order for 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.
|
||||
"""
|
||||
# pylint: disable=assignment-from-no-return
|
||||
prefix = text.startswith(self.COMMAND_PREFIX)
|
||||
length = len(text) > len(self.COMMAND_PREFIX)
|
||||
if not (prefix and length):
|
||||
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):
|
||||
cmd = self.get_command(name)
|
||||
|
||||
args, opts = parse_arguments(arguments) if arguments else ([], [])
|
||||
args, kwargs = adapt_arguments(cmd, arguments, args, opts)
|
||||
|
||||
if self.command_preprocessor(cmd, name, arguments, args, kwargs):
|
||||
return
|
||||
value = cmd(self, *args, **kwargs)
|
||||
self.command_postprocessor(cmd, name, arguments, args, kwargs, value)
|
||||
|
||||
def command_preprocessor(self, cmd, 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.
|
||||
"""
|
||||
|
||||
def command_postprocessor(self, cmd, name, arguments, args, kwargs, value):
|
||||
"""
|
||||
Redefine this method in the subclass to execute custom code
|
||||
after command gets executed.
|
||||
"""
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def get_command(self, name):
|
||||
cmd = get_command(self.COMMAND_HOST, name)
|
||||
if not cmd:
|
||||
raise NoCommandError("Command does not exist", name=name)
|
||||
return cmd
|
||||
|
||||
def list_commands(self):
|
||||
commands = list_commands(self.COMMAND_HOST)
|
||||
commands = dict(commands)
|
||||
return sorted(set(commands.values()), key=lambda k: k.__repr__())
|
||||
|
||||
|
||||
class Command:
|
||||
|
||||
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.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
try:
|
||||
return self.handler(*args, **kwargs)
|
||||
|
||||
# This allows to use a shortcut 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 as 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):
|
||||
if self.first_name > other.first_name:
|
||||
return 1
|
||||
if self.first_name < other.first_name:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
@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.
|
||||
"""
|
||||
return getdoc(self.handler)
|
||||
|
||||
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) # pylint: disable=W1505
|
||||
|
||||
# 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 native name (the one
|
||||
extracted from the command handler) will be used.
|
||||
|
||||
If 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=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)
|
||||
|
||||
native = properties.get('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 = properties.get('expand', 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': expand
|
||||
}
|
||||
|
||||
def decorator(handler):
|
||||
"""
|
||||
Decorator which receives handler as a first argument and then
|
||||
wraps it in the command which then returns back.
|
||||
"""
|
||||
cmd = Command(handler, *names, **properties)
|
||||
|
||||
# Extract and inject a native name if either no other names are
|
||||
# specified or native property is enabled, while making
|
||||
# sure it is going to be the first one in the list.
|
||||
if not names or native:
|
||||
names.insert(0, cmd.native_name)
|
||||
cmd.names = tuple(names)
|
||||
|
||||
return cmd
|
||||
|
||||
# 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 doc(text):
|
||||
"""
|
||||
This decorator is used to bind a documentation (a help) to a
|
||||
command.
|
||||
"""
|
||||
def decorator(target):
|
||||
if isinstance(target, Command):
|
||||
target.handler.__doc__ = text
|
||||
else:
|
||||
target.__doc__ = text
|
||||
return target
|
||||
|
||||
return decorator
|
20
gajim/command_system/implementation/__init__.py
Normal file
20
gajim/command_system/implementation/__init__.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@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 implementation and auxiliary systems which implement the standard
|
||||
Gajim commands and also provide an infrastructure for adding custom
|
||||
commands.
|
||||
"""
|
131
gajim/command_system/implementation/custom.py
Normal file
131
gajim/command_system/implementation/custom.py
Normal file
|
@ -0,0 +1,131 @@
|
|||
# Copyright (c) 2009-2010, Alexander Cherniuk (ts33kr@gmail.com)
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
#
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
This module contains examples of how to create your own commands, by
|
||||
creating a new command container, bounded to a specific command host,
|
||||
and defining a set of commands inside of it.
|
||||
|
||||
Keep in mind that this module is not being loaded from anywhere, so the
|
||||
code in here will not be executed and commands defined here will not be
|
||||
detected.
|
||||
"""
|
||||
|
||||
from gajim.common.i18n import _
|
||||
from gajim.command_system.framework import CommandContainer
|
||||
from gajim.command_system.framework import command
|
||||
from gajim.command_system.framework import doc
|
||||
from gajim.command_system.implementation.hosts import ChatCommands
|
||||
from gajim.command_system.implementation.hosts import PrivateChatCommands
|
||||
from gajim.command_system.implementation.hosts import GroupChatCommands
|
||||
|
||||
|
||||
class CustomCommonCommands(CommandContainer):
|
||||
"""
|
||||
The AUTOMATIC class variable, set to a positive value, instructs the
|
||||
command system to automatically discover the command container and
|
||||
enable it.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
AUTOMATIC = True
|
||||
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 programmatically 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, if you want.
|
||||
"""
|
||||
return "I don't dance."
|
||||
|
||||
|
||||
class CustomChatCommands(CommandContainer):
|
||||
"""
|
||||
This command container bounds only to the ChatCommands command host.
|
||||
Therefore commands defined inside of the container will be available
|
||||
only to a chat.
|
||||
"""
|
||||
|
||||
AUTOMATIC = True
|
||||
HOSTS = (ChatCommands,)
|
||||
|
||||
@command("squal", "bawl")
|
||||
def sing(self):
|
||||
"""
|
||||
This command has an additional aliases. It means the command will
|
||||
be available under three names: sing (the native name), squal
|
||||
(the first alias), bawl (the second alias).
|
||||
|
||||
You can turn off the usage of the native name, if you want, and
|
||||
specify a name or a set of names, as aliases, under which a
|
||||
command will be available.
|
||||
"""
|
||||
return "Buy yourself a stereo."
|
||||
|
||||
|
||||
class CustomPrivateChatCommands(CommandContainer):
|
||||
"""
|
||||
This command container bounds only to the PrivateChatCommands
|
||||
command host. Therefore commands defined inside of the container
|
||||
will be available only to a private chat.
|
||||
"""
|
||||
|
||||
AUTOMATIC = True
|
||||
HOSTS = (PrivateChatCommands,)
|
||||
|
||||
@command
|
||||
#Example string. Do not translate
|
||||
@doc(_("The same as using a doc-string, except it supports translation"))
|
||||
def make_coffee(self):
|
||||
return "I'm not a coffee machine!"
|
||||
|
||||
|
||||
class CustomGroupChatCommands(CommandContainer):
|
||||
"""
|
||||
This command container bounds only to the GroupChatCommands command
|
||||
host. Therefore commands defined inside of the container will be
|
||||
available only to a group chat.
|
||||
"""
|
||||
|
||||
AUTOMATIC = True
|
||||
HOSTS = (GroupChatCommands,)
|
||||
|
||||
@command
|
||||
def fetch(self):
|
||||
return "Buy yourself a dog."
|
136
gajim/command_system/implementation/execute.py
Normal file
136
gajim/command_system/implementation/execute.py
Normal file
|
@ -0,0 +1,136 @@
|
|||
# Copyright (c) 2010, Alexander Cherniuk (ts33kr@gmail.com)
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
#
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
Provides facilities to safely execute expressions inside a shell process
|
||||
and capture the resulting output, in an asynchronous fashion, avoiding
|
||||
deadlocks. If the process execution time reaches the threshold - it is
|
||||
forced to terminate. Consists of a tiny framework and a couple of
|
||||
commands as a frontend.
|
||||
"""
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from os.path import expanduser
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.i18n import _
|
||||
from gajim.command_system.framework import CommandContainer
|
||||
from gajim.command_system.framework import command
|
||||
from gajim.command_system.framework import doc
|
||||
from gajim.command_system.implementation.hosts import ChatCommands
|
||||
from gajim.command_system.implementation.hosts import PrivateChatCommands
|
||||
from gajim.command_system.implementation.hosts import GroupChatCommands
|
||||
|
||||
|
||||
class Execute(CommandContainer):
|
||||
AUTOMATIC = True
|
||||
HOSTS = ChatCommands, PrivateChatCommands, GroupChatCommands
|
||||
|
||||
DIRECTORY = "~"
|
||||
|
||||
POLL_INTERVAL = 100
|
||||
POLL_COUNT = 5
|
||||
|
||||
@command("exec", raw=True)
|
||||
@doc(_("Execute expression inside a shell, show output"))
|
||||
def execute(self, expression):
|
||||
Execute.spawn(self, expression)
|
||||
|
||||
@classmethod
|
||||
def spawn(cls, processor, expression):
|
||||
command_system_execute = app.settings.get('command_system_execute')
|
||||
if command_system_execute:
|
||||
pipes = dict(stdout=PIPE, stderr=PIPE)
|
||||
directory = expanduser(cls.DIRECTORY)
|
||||
popen = Popen(expression, shell=True, cwd=directory, **pipes)
|
||||
cls.monitor(processor, popen)
|
||||
else:
|
||||
processor.echo_error(
|
||||
_('Command disabled. This command can be enabled by '
|
||||
'setting \'command_system_execute\' to True in ACE '
|
||||
'(Advanced Configuration Editor).'))
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def monitor(cls, processor, popen):
|
||||
poller = cls.poller(processor, popen)
|
||||
GLib.timeout_add(cls.POLL_INTERVAL, next, poller)
|
||||
|
||||
@classmethod
|
||||
def poller(cls, processor, popen):
|
||||
for _ in range(cls.POLL_COUNT):
|
||||
yield cls.brush(processor, popen)
|
||||
cls.overdue(processor, popen)
|
||||
yield False
|
||||
|
||||
@classmethod
|
||||
def brush(cls, processor, popen):
|
||||
if popen.poll() is not None:
|
||||
cls.terminated(processor, popen)
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def terminated(cls, processor, popen):
|
||||
stdout, stderr = cls.fetch(popen)
|
||||
success = popen.returncode == 0
|
||||
if success and stdout:
|
||||
processor.echo(stdout)
|
||||
elif not success and stderr:
|
||||
processor.echo_error(stderr)
|
||||
|
||||
@classmethod
|
||||
def overdue(cls, processor, popen):
|
||||
popen.terminate()
|
||||
|
||||
@classmethod
|
||||
def fetch(cls, popen):
|
||||
data = popen.communicate()
|
||||
return map(cls.clean, data)
|
||||
|
||||
@staticmethod
|
||||
def clean(text):
|
||||
strip = chr(10) + chr(32)
|
||||
return text.decode().strip(strip)
|
||||
|
||||
|
||||
class Show(Execute):
|
||||
|
||||
@command("sh", raw=True)
|
||||
@doc(_("Execute expression inside a shell, send output"))
|
||||
def show(self, expression):
|
||||
Show.spawn(self, expression)
|
||||
|
||||
@classmethod
|
||||
def terminated(cls, processor, popen):
|
||||
stdout, stderr = cls.fetch(popen)
|
||||
success = popen.returncode == 0
|
||||
if success and stdout:
|
||||
processor.send(stdout)
|
||||
elif not success and stderr:
|
||||
processor.echo_error(stderr)
|
45
gajim/command_system/implementation/hosts.py
Normal file
45
gajim/command_system/implementation/hosts.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@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 gajim.command_system.framework import CommandHost
|
||||
|
||||
|
||||
class ChatCommands(CommandHost):
|
||||
"""
|
||||
This command host is bound to the command processor which processes
|
||||
commands from a chat.
|
||||
"""
|
||||
AUTOMATIC = True
|
||||
|
||||
|
||||
class PrivateChatCommands(CommandHost):
|
||||
"""
|
||||
This command host is bound to the command processor which processes
|
||||
commands from a private chat.
|
||||
"""
|
||||
AUTOMATIC = True
|
||||
|
||||
|
||||
class GroupChatCommands(CommandHost):
|
||||
"""
|
||||
This command host is bound to the command processor which processes
|
||||
commands from a group chat.
|
||||
"""
|
||||
AUTOMATIC = True
|
195
gajim/command_system/implementation/middleware.py
Normal file
195
gajim/command_system/implementation/middleware.py
Normal file
|
@ -0,0 +1,195 @@
|
|||
# Copyright (c) 2009-2010, Alexander Cherniuk (ts33kr@gmail.com)
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
#
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
Provides a glue to tie command system framework and the actual code
|
||||
where it would be dropped in. Defines a little bit of scaffolding to
|
||||
support interaction between the two and a few utility methods so you
|
||||
don't need to dig up the code itself to write basic commands.
|
||||
"""
|
||||
|
||||
from traceback import print_exc
|
||||
|
||||
from gi.repository import Pango
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.i18n import _
|
||||
|
||||
from gajim.command_system.framework import CommandProcessor
|
||||
from gajim.command_system.errors import CommandError
|
||||
from gajim.command_system.errors import NoCommandError
|
||||
|
||||
|
||||
class ChatCommandProcessor(CommandProcessor):
|
||||
"""
|
||||
A basic scaffolding to provide convenient interaction between the
|
||||
command system and chat controls. It will be merged directly into
|
||||
the controls, by ChatCommandProcessor being among superclasses of
|
||||
the controls.
|
||||
"""
|
||||
|
||||
def process_as_command(self, text):
|
||||
self.command_succeeded = False
|
||||
parents = super(ChatCommandProcessor, self)
|
||||
flag = parents.process_as_command(text)
|
||||
if flag and self.command_succeeded:
|
||||
self.add_history(text)
|
||||
self.clear_input()
|
||||
return flag
|
||||
|
||||
def execute_command(self, name, arguments):
|
||||
try:
|
||||
parents = super(ChatCommandProcessor, self)
|
||||
parents.execute_command(name, arguments)
|
||||
except NoCommandError as error:
|
||||
details = dict(name=error.name, message=error.message)
|
||||
message = "%(name)s: %(message)s\n" % details
|
||||
message += "Try using the //%(name)s or /say /%(name)s " % details
|
||||
message += "construct if you intended to send it as a text."
|
||||
self.echo_error(message)
|
||||
except CommandError as error:
|
||||
self.echo_error("%s: %s" % (error.name, error.message))
|
||||
except Exception:
|
||||
self.echo_error(_("Error during command execution!"))
|
||||
print_exc()
|
||||
else:
|
||||
self.command_succeeded = True
|
||||
|
||||
def looks_like_command(self, text, body, name, arguments):
|
||||
# Command escape stuff goes here. If text was prepended by the
|
||||
# command prefix twice, like //not_a_command (if prefix is set
|
||||
# to /) then it will be escaped, that is sent just as a regular
|
||||
# message with one (only one) prefix removed, so message will be
|
||||
# /not_a_command.
|
||||
if body.startswith(self.COMMAND_PREFIX):
|
||||
self.send(body)
|
||||
return True
|
||||
|
||||
def command_preprocessor(self, command, name, arguments, args, kwargs):
|
||||
# If command argument contain h or help option - forward it to
|
||||
# the /help command. Don't 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:
|
||||
help_ = self.get_command('help')
|
||||
self.echo(help_(self, name))
|
||||
return True
|
||||
|
||||
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, str):
|
||||
self.echo(value)
|
||||
|
||||
|
||||
class CommandTools:
|
||||
"""
|
||||
Contains a set of basic tools and shortcuts you can use in your
|
||||
commands to perform some simple operations. These will be merged
|
||||
directly into the controls, by CommandTools being among superclasses
|
||||
of the controls.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.install_tags()
|
||||
|
||||
def install_tags(self):
|
||||
buffer_ = self.conv_textview.tv.get_buffer()
|
||||
|
||||
name = "Monospace"
|
||||
font = Pango.FontDescription(name)
|
||||
|
||||
command_ok_tag = buffer_.create_tag("command_ok")
|
||||
command_ok_tag.set_property("font-desc", font)
|
||||
command_ok_tag.set_property("foreground", "#3465A4")
|
||||
|
||||
command_error_tag = buffer_.create_tag("command_error")
|
||||
command_error_tag.set_property("font-desc", font)
|
||||
command_error_tag.set_property("foreground", "#F57900")
|
||||
|
||||
def shift_line(self):
|
||||
buffer_ = self.conv_textview.tv.get_buffer()
|
||||
iter_ = buffer_.get_end_iter()
|
||||
if iter_.ends_line() and not iter_.is_start():
|
||||
buffer_.insert_with_tags_by_name(iter_, "\n", "eol")
|
||||
|
||||
def append_with_tags(self, text, *tags):
|
||||
buffer_ = self.conv_textview.tv.get_buffer()
|
||||
iter_ = buffer_.get_end_iter()
|
||||
buffer_.insert_with_tags_by_name(iter_, text, *tags)
|
||||
|
||||
def echo(self, text, tag="command_ok"):
|
||||
"""
|
||||
Print given text to the user, as a regular command output.
|
||||
"""
|
||||
self.shift_line()
|
||||
self.append_with_tags(text, tag)
|
||||
|
||||
def echo_error(self, text):
|
||||
"""
|
||||
Print given text to the user, as an error command output.
|
||||
"""
|
||||
self.echo(text, "command_error")
|
||||
|
||||
def send(self, text):
|
||||
"""
|
||||
Send a message to the contact.
|
||||
"""
|
||||
self.send_message(text, process_commands=False)
|
||||
|
||||
def set_input(self, text):
|
||||
"""
|
||||
Set given text into the input.
|
||||
"""
|
||||
buffer = self.msg_textview.get_buffer()
|
||||
buffer.set_text(text)
|
||||
|
||||
def clear_input(self):
|
||||
"""
|
||||
Clear input.
|
||||
"""
|
||||
self.set_input(str())
|
||||
|
||||
def add_history(self, text):
|
||||
"""
|
||||
Add given text to the input history, so user can scroll through
|
||||
it using ctrl + up/down arrow keys.
|
||||
"""
|
||||
self.save_message(text, 'sent')
|
||||
|
||||
@property
|
||||
def connection(self):
|
||||
"""
|
||||
Get the current connection object.
|
||||
"""
|
||||
return app.connections[self.account]
|
||||
|
||||
@property
|
||||
def full_jid(self):
|
||||
"""
|
||||
Get a full JID of the contact.
|
||||
"""
|
||||
return self.contact.get_full_jid()
|
433
gajim/command_system/implementation/standard.py
Normal file
433
gajim/command_system/implementation/standard.py
Normal file
|
@ -0,0 +1,433 @@
|
|||
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@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 an actual implementation for the standard commands.
|
||||
"""
|
||||
|
||||
from time import localtime
|
||||
from time import strftime
|
||||
from datetime import date
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common import helpers
|
||||
from gajim.common.i18n import _
|
||||
from gajim.common.const import KindConstant
|
||||
|
||||
from gajim.command_system.errors import CommandError
|
||||
from gajim.command_system.framework import CommandContainer
|
||||
from gajim.command_system.framework import command
|
||||
from gajim.command_system.framework import doc
|
||||
from gajim.command_system.mapping import generate_usage
|
||||
|
||||
from gajim.command_system.implementation.hosts import ChatCommands
|
||||
from gajim.command_system.implementation.hosts import PrivateChatCommands
|
||||
from gajim.command_system.implementation.hosts import GroupChatCommands
|
||||
|
||||
|
||||
class StandardCommonCommands(CommandContainer):
|
||||
"""
|
||||
This command container contains standard commands which are common
|
||||
to all - chat, private chat, group chat.
|
||||
"""
|
||||
|
||||
AUTOMATIC = True
|
||||
HOSTS = ChatCommands, PrivateChatCommands, GroupChatCommands
|
||||
|
||||
@command(overlap=True)
|
||||
@doc(_("Show help on a given command or a list of available commands if "
|
||||
"-a is given"))
|
||||
def help(self, cmd=None, all_=False):
|
||||
if cmd:
|
||||
cmd = self.get_command(cmd)
|
||||
|
||||
documentation = _(cmd.extract_documentation())
|
||||
usage = generate_usage(cmd)
|
||||
|
||||
text = []
|
||||
|
||||
if documentation:
|
||||
text.append(documentation)
|
||||
if cmd.usage:
|
||||
text.append(usage)
|
||||
|
||||
return '\n\n'.join(text)
|
||||
|
||||
if all_:
|
||||
for cmd_ in self.list_commands():
|
||||
names = ', '.join(cmd_.names)
|
||||
description = cmd_.extract_description()
|
||||
|
||||
self.echo("%s - %s" % (names, description))
|
||||
else:
|
||||
help_ = self.get_command('help')
|
||||
self.echo(help_(self, 'help'))
|
||||
|
||||
@command(raw=True)
|
||||
@doc(_("Send a message to the contact"))
|
||||
def say(self, message):
|
||||
self.send(message)
|
||||
|
||||
@command(raw=True)
|
||||
@doc(_("Send action (in the third person) to the current chat"))
|
||||
def me(self, action):
|
||||
self.send("/me %s" % action)
|
||||
|
||||
@command('lastlog', overlap=True)
|
||||
@doc(_("Show logged messages which mention given text"))
|
||||
def grep(self, text, limit=None):
|
||||
results = app.storage.archive.search_log(self.account, self.contact.jid, text)
|
||||
|
||||
if not results:
|
||||
raise CommandError(_("%s: Nothing found") % text)
|
||||
|
||||
if limit:
|
||||
try:
|
||||
results = results[len(results) - int(limit):]
|
||||
except ValueError:
|
||||
raise CommandError(_("Limit must be an integer"))
|
||||
|
||||
for row in results:
|
||||
contact = row.contact_name
|
||||
if not contact:
|
||||
if row.kind == KindConstant.CHAT_MSG_SENT:
|
||||
contact = app.nicks[self.account]
|
||||
else:
|
||||
contact = self.contact.name
|
||||
|
||||
time_obj = localtime(row.time)
|
||||
date_obj = date.fromtimestamp(row.time)
|
||||
date_ = strftime('%Y-%m-%d', time_obj)
|
||||
time_ = strftime('%H:%M:%S', time_obj)
|
||||
|
||||
if date_obj == date.today():
|
||||
formatted = "[%s] %s: %s" % (time_, contact, row.message)
|
||||
else:
|
||||
formatted = "[%s, %s] %s: %s" % (
|
||||
date_, time_, contact, row.message)
|
||||
|
||||
self.echo(formatted)
|
||||
|
||||
@command(raw=True, empty=True)
|
||||
# Do not translate online, away, chat, xa, dnd
|
||||
@doc(_("""
|
||||
Set the current status
|
||||
|
||||
Status can be given as one of the following values:
|
||||
online, away, chat, xa, dnd.
|
||||
"""))
|
||||
def status(self, status, message):
|
||||
if status not in ('online', 'away', 'chat', 'xa', 'dnd'):
|
||||
raise CommandError("Invalid status given")
|
||||
for connection in app.connections.values():
|
||||
if not app.settings.get_account_setting(connection.name,
|
||||
'sync_with_global_status'):
|
||||
continue
|
||||
if not connection.state.is_available:
|
||||
continue
|
||||
connection.change_status(status, message)
|
||||
|
||||
@command(raw=True, empty=True)
|
||||
@doc(_("Set the current status to away"))
|
||||
def away(self, message):
|
||||
if not message:
|
||||
message = _("Away")
|
||||
|
||||
for connection in app.connections.values():
|
||||
if not app.settings.get_account_setting(connection.name,
|
||||
'sync_with_global_status'):
|
||||
continue
|
||||
if not connection.state.is_available:
|
||||
continue
|
||||
connection.change_status('away', message)
|
||||
|
||||
@command('back', raw=True, empty=True)
|
||||
@doc(_("Set the current status to online"))
|
||||
def online(self, message):
|
||||
if not message:
|
||||
message = _("Available")
|
||||
|
||||
for connection in app.connections.values():
|
||||
if not app.settings.get_account_setting(connection.name,
|
||||
'sync_with_global_status'):
|
||||
continue
|
||||
if not connection.state.is_available:
|
||||
continue
|
||||
connection.change_status('online', message)
|
||||
|
||||
@command
|
||||
@doc(_("Send a disco info request"))
|
||||
def disco(self):
|
||||
client = app.get_client(self.account)
|
||||
if not client.state.is_available:
|
||||
return
|
||||
|
||||
client.get_module('Discovery').disco_contact(self.contact)
|
||||
|
||||
|
||||
class StandardCommonChatCommands(CommandContainer):
|
||||
"""
|
||||
This command container contains standard commands, which are common
|
||||
to a chat and a private chat only.
|
||||
"""
|
||||
|
||||
AUTOMATIC = True
|
||||
HOSTS = ChatCommands, PrivateChatCommands
|
||||
|
||||
@command
|
||||
@doc(_("Clear the text window"))
|
||||
def clear(self):
|
||||
self.conv_textview.clear()
|
||||
|
||||
@command
|
||||
@doc(_("Send a ping to the contact"))
|
||||
def ping(self):
|
||||
if self.account == app.ZEROCONF_ACC_NAME:
|
||||
raise CommandError(
|
||||
_('Command is not supported for zeroconf accounts'))
|
||||
app.connections[self.account].get_module('Ping').send_ping(self.contact)
|
||||
|
||||
@command
|
||||
@doc(_("Send DTMF sequence through an open voice chat"))
|
||||
def dtmf(self, sequence):
|
||||
if not self.audio_sid:
|
||||
raise CommandError(_("No open voice chats with the contact"))
|
||||
for tone in sequence:
|
||||
if not (tone in ("*", "#") or tone.isdigit()):
|
||||
raise CommandError(_("%s is not a valid tone") % tone)
|
||||
gjs = self.connection.get_module('Jingle').get_jingle_session
|
||||
session = gjs(self.full_jid, self.audio_sid)
|
||||
content = session.get_content("audio")
|
||||
content.batch_dtmf(sequence)
|
||||
|
||||
@command
|
||||
@doc(_("Toggle Voice Chat"))
|
||||
def audio(self):
|
||||
if not self.audio_available:
|
||||
raise CommandError(_("Voice chats are not available"))
|
||||
# An audio session is toggled by inverting the state of the
|
||||
# appropriate button.
|
||||
state = self._audio_button.get_active()
|
||||
self._audio_button.set_active(not state)
|
||||
|
||||
@command
|
||||
@doc(_("Toggle Video Chat"))
|
||||
def video(self):
|
||||
if not self.video_available:
|
||||
raise CommandError(_("Video chats are not available"))
|
||||
# A video session is toggled by inverting the state of the
|
||||
# appropriate button.
|
||||
state = self._video_button.get_active()
|
||||
self._video_button.set_active(not state)
|
||||
|
||||
@command(raw=True)
|
||||
@doc(_("Send a message to the contact that will attract their attention"))
|
||||
def attention(self, message):
|
||||
self.send_message(message, process_commands=False, attention=True)
|
||||
|
||||
|
||||
class StandardChatCommands(CommandContainer):
|
||||
"""
|
||||
This command container contains standard commands which are unique
|
||||
to a chat.
|
||||
"""
|
||||
|
||||
AUTOMATIC = True
|
||||
HOSTS = (ChatCommands,)
|
||||
|
||||
|
||||
class StandardPrivateChatCommands(CommandContainer):
|
||||
"""
|
||||
This command container contains standard commands which are unique
|
||||
to a private chat.
|
||||
"""
|
||||
|
||||
AUTOMATIC = True
|
||||
HOSTS = (PrivateChatCommands,)
|
||||
|
||||
|
||||
class StandardGroupChatCommands(CommandContainer):
|
||||
"""
|
||||
This command container contains standard commands which are unique
|
||||
to a group chat.
|
||||
"""
|
||||
|
||||
AUTOMATIC = True
|
||||
HOSTS = (GroupChatCommands,)
|
||||
|
||||
@command
|
||||
@doc(_("Clear the text window"))
|
||||
def clear(self):
|
||||
self.conv_textview.clear()
|
||||
|
||||
@command(raw=True)
|
||||
@doc(_("Change your nickname in a group chat"))
|
||||
def nick(self, new_nick):
|
||||
try:
|
||||
new_nick = helpers.parse_resource(new_nick)
|
||||
except Exception:
|
||||
raise CommandError(_("Invalid nickname"))
|
||||
# FIXME: Check state of MUC
|
||||
self.connection.get_module('MUC').change_nick(
|
||||
self.room_jid, new_nick)
|
||||
self.new_nick = new_nick
|
||||
|
||||
@command('query', raw=True)
|
||||
@doc(_("Open a private chat window with a specified participant"))
|
||||
def chat(self, nick):
|
||||
nicks = app.contacts.get_nick_list(self.account, self.room_jid)
|
||||
if nick in nicks:
|
||||
self.send_pm(nick)
|
||||
else:
|
||||
raise CommandError(_("Nickname not found"))
|
||||
|
||||
@command('msg', raw=True)
|
||||
@doc(_("Open a private chat window with a specified participant and send "
|
||||
"him a message"))
|
||||
def message(self, nick, message):
|
||||
nicks = app.contacts.get_nick_list(self.account, self.room_jid)
|
||||
if nick in nicks:
|
||||
self.send_pm(nick, message)
|
||||
else:
|
||||
raise CommandError(_("Nickname not found"))
|
||||
|
||||
@command(raw=True, empty=True)
|
||||
@doc(_("Display or change a group chat topic"))
|
||||
def topic(self, new_topic):
|
||||
if new_topic:
|
||||
self.connection.get_module('MUC').set_subject(
|
||||
self.room_jid, new_topic)
|
||||
else:
|
||||
return self.subject
|
||||
|
||||
@command(raw=True, empty=True)
|
||||
@doc(_("Invite a user to a group chat for a reason"))
|
||||
def invite(self, jid, reason):
|
||||
control = app.get_groupchat_control(self.account, self.room_jid)
|
||||
if control is not None:
|
||||
control.invite(jid)
|
||||
|
||||
@command(raw=True, empty=True)
|
||||
@doc(_("Join a group chat given by an XMPP Address"))
|
||||
def join(self, jid):
|
||||
if '@' not in jid:
|
||||
jid = jid + '@' + app.get_server_from_jid(self.room_jid)
|
||||
|
||||
app.app.activate_action(
|
||||
'groupchat-join',
|
||||
GLib.Variant('as', [self.account, jid]))
|
||||
|
||||
@command('part', 'close', raw=True, empty=True)
|
||||
@doc(_("Leave the group chat, optionally giving a reason, and close tab or "
|
||||
"window"))
|
||||
def leave(self, reason):
|
||||
self.leave(reason=reason)
|
||||
|
||||
@command(raw=True, empty=True)
|
||||
@doc(_("""
|
||||
Ban user by a nick or a JID from a groupchat
|
||||
|
||||
If given nickname is not found it will be treated as a JID.
|
||||
"""))
|
||||
def ban(self, who, reason=''):
|
||||
if who in app.contacts.get_nick_list(self.account, self.room_jid):
|
||||
contact = app.contacts.get_gc_contact(
|
||||
self.account, self.room_jid, who)
|
||||
who = contact.jid
|
||||
self.connection.get_module('MUC').set_affiliation(
|
||||
self.room_jid,
|
||||
{who: {'affiliation': 'outcast',
|
||||
'reason': reason}})
|
||||
|
||||
@command(raw=True, empty=True)
|
||||
@doc(_("Kick user from group chat by nickname"))
|
||||
def kick(self, who, reason):
|
||||
if who not in app.contacts.get_nick_list(self.account, self.room_jid):
|
||||
raise CommandError(_("Nickname not found"))
|
||||
self.connection.get_module('MUC').set_role(
|
||||
self.room_jid, who, 'none', reason)
|
||||
|
||||
@command(raw=True)
|
||||
# Do not translate moderator, participant, visitor, none
|
||||
@doc(_("""Set participant role in group chat.
|
||||
Role can be given as one of the following values:
|
||||
moderator, participant, visitor, none"""))
|
||||
def role(self, who, role):
|
||||
if role not in ('moderator', 'participant', 'visitor', 'none'):
|
||||
raise CommandError(_("Invalid role given"))
|
||||
if who not in app.contacts.get_nick_list(self.account, self.room_jid):
|
||||
raise CommandError(_("Nickname not found"))
|
||||
self.connection.get_module('MUC').set_role(self.room_jid, who, role)
|
||||
|
||||
@command(raw=True)
|
||||
# Do not translate owner, admin, member, outcast, none
|
||||
@doc(_("""Set participant affiliation in group chat.
|
||||
Affiliation can be given as one of the following values:
|
||||
owner, admin, member, outcast, none"""))
|
||||
def affiliate(self, who, affiliation):
|
||||
if affiliation not in ('owner', 'admin', 'member', 'outcast', 'none'):
|
||||
raise CommandError(_("Invalid affiliation given"))
|
||||
if who not in app.contacts.get_nick_list(self.account, self.room_jid):
|
||||
raise CommandError(_("Nickname not found"))
|
||||
contact = app.contacts.get_gc_contact(self.account, self.room_jid, who)
|
||||
|
||||
self.connection.get_module('MUC').set_affiliation(
|
||||
self.room_jid,
|
||||
{contact.jid: {'affiliation': affiliation}})
|
||||
|
||||
@command
|
||||
@doc(_("Display names of all group chat participants"))
|
||||
def names(self, verbose=False):
|
||||
ggc = app.contacts.get_gc_contact
|
||||
gnl = app.contacts.get_nick_list
|
||||
|
||||
get_contact = lambda nick: ggc(self.account, self.room_jid, nick)
|
||||
get_role = lambda nick: get_contact(nick).role
|
||||
nicks = gnl(self.account, self.room_jid)
|
||||
|
||||
nicks = sorted(nicks)
|
||||
nicks = sorted(nicks, key=get_role)
|
||||
|
||||
if not verbose:
|
||||
return ", ".join(nicks)
|
||||
|
||||
for nick in nicks:
|
||||
contact = get_contact(nick)
|
||||
role = helpers.get_uf_role(contact.role)
|
||||
affiliation = helpers.get_uf_affiliation(contact.affiliation)
|
||||
self.echo("%s - %s - %s" % (nick, role, affiliation))
|
||||
|
||||
@command('ignore', raw=True)
|
||||
@doc(_("Forbid a participant to send you public or private messages"))
|
||||
def block(self, who):
|
||||
self.on_block(None, who)
|
||||
|
||||
@command('unignore', raw=True)
|
||||
@doc(_("Allow a participant to send you public or private messages"))
|
||||
def unblock(self, who):
|
||||
self.on_unblock(None, who)
|
||||
|
||||
@command
|
||||
@doc(_("Send a ping to the contact"))
|
||||
def ping(self, nick):
|
||||
if self.account == app.ZEROCONF_ACC_NAME:
|
||||
raise CommandError(
|
||||
_('Command is not supported for zeroconf accounts'))
|
||||
gc_c = app.contacts.get_gc_contact(self.account, self.room_jid, nick)
|
||||
if gc_c is None:
|
||||
raise CommandError(_("Unknown nickname"))
|
||||
app.connections[self.account].get_module('Ping').send_ping(gc_c)
|
349
gajim/command_system/mapping.py
Normal file
349
gajim/command_system/mapping.py
Normal file
|
@ -0,0 +1,349 @@
|
|||
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@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 positional 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 operator import itemgetter
|
||||
|
||||
from gajim.common.i18n import _
|
||||
|
||||
from gajim.command_system.errors import DefinitionError
|
||||
from gajim.command_system.errors import 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.
|
||||
"""
|
||||
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 positional 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 (supposedly 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:
|
||||
# Substitute 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 associable state.
|
||||
# Expanding short, one-letter options to a verbose ones, if
|
||||
# corresponding opt-in has been given.
|
||||
if command.expand:
|
||||
expanded = []
|
||||
for spec_key in norm_kwargs.keys():
|
||||
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), bool):
|
||||
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 = list(map(lambda t: t[0], args))
|
||||
opts = list(map(lambda t: (t[0], t[1]), 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 proper further processing.
|
||||
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(_("Too many 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, bool):
|
||||
if not isinstance(value, bool):
|
||||
raise CommandError(
|
||||
"%s: Switch can not take an argument" % key, command)
|
||||
|
||||
# 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 completely.
|
||||
"""
|
||||
spec_args, spec_kwargs, var_args, var_kwargs = command.extract_specification()
|
||||
|
||||
# Remove some special positional arguments from the specification,
|
||||
# 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, bool):
|
||||
value = str()
|
||||
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
|
34
gajim/command_system/tools.py
Normal file
34
gajim/command_system/tools.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
# Copyright (c) 2010, Alexander Cherniuk (ts33kr@gmail.com)
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
#
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
def remove(sequence, target):
|
||||
if isinstance(sequence, list):
|
||||
if target in sequence:
|
||||
sequence.remove(target)
|
||||
elif isinstance(sequence, dict):
|
||||
if target in sequence:
|
||||
del sequence[target]
|
Loading…
Add table
Add a link
Reference in a new issue