9578053 Jan 22 2022 distfiles.gentoo.org/distfiles/gajim-1.3.3-2.tar.gz

This commit is contained in:
emdee 2022-10-19 18:09:31 +00:00
parent a5b3822651
commit 4c1b226bff
1045 changed files with 753037 additions and 18 deletions

View 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.
"""

View 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)

View 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.
"""

View 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

View 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.
"""

View 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."

View 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)

View 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

View 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()

View 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)

View 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

View 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]