diff --git a/src/command_system/dispatcher.py b/src/command_system/dispatcher.py new file mode 100644 index 0000000000000000000000000000000000000000..cc9f0ea7a6638405dfc8ad81844e4f4e7a956a55 --- /dev/null +++ b/src/command_system/dispatcher.py @@ -0,0 +1,117 @@ +# 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 types import NoneType +from tools import remove + +COMMANDS = {} +CONTAINERS = {} + +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 framework import Command + return isinstance(attribute, Command) + +def is_root(namespace): + metaclass = namespace.get("__metaclass__", NoneType) + 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.iteritems(): + yield name, command + +class Dispatchable(type): + + def __init__(self, name, bases, namespace): + parents = super(Dispatchable, self) + parents.__init__(name, bases, namespace) + if not is_root(namespace): + self.dispatch() + + def dispatch(self): + if self.AUTOMATIC: + self.enable() + +class Host(Dispatchable): + + def enable(self): + add_host(self) + + def disable(self): + remove_host(self) + +class Container(Dispatchable): + + def enable(self): + add_container(self) + add_commands(self) + + def disable(self): + remove_commands(self) + remove_container(self) \ No newline at end of file diff --git a/src/command_system/dispatching.py b/src/command_system/dispatching.py deleted file mode 100644 index d19581429c63c1ecb574a7fec618457615c4ff20..0000000000000000000000000000000000000000 --- a/src/command_system/dispatching.py +++ /dev/null @@ -1,90 +0,0 @@ -# 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 backbone of the command system. Provides automatic dispatching which -does not require explicit registering commands or containers and remains -active even after everything is done, so new commands can be added -during the runtime. -""" - -from types import NoneType - -class Dispatcher(type): - - containers = {} - commands = {} - - @classmethod - def register_host(cls, host): - cls.containers[host] = [] - - @classmethod - def register_container(cls, container): - for host in container.HOSTS: - cls.containers[host].append(container) - - @classmethod - def register_commands(cls, container): - cls.commands[container] = {} - for command in cls.traverse_commands(container): - for name in command.names: - cls.commands[container][name] = command - - @classmethod - def get_command(cls, host, name): - for container in cls.containers[host]: - command = cls.commands[container].get(name) - if command: - return command - - @classmethod - def list_commands(cls, host): - for container in cls.containers[host]: - commands = cls.commands[container] - for name, command in commands.iteritems(): - yield name, command - - @classmethod - def traverse_commands(cls, container): - for name in dir(container): - attribute = getattr(container, name) - if cls.is_command(attribute): - yield attribute - - @staticmethod - def is_root(ns): - metaclass = ns.get('__metaclass__', NoneType) - return issubclass(metaclass, Dispatcher) - - @staticmethod - def is_command(attribute): - from framework import Command - return isinstance(attribute, Command) - -class HostDispatcher(Dispatcher): - - def __init__(self, name, bases, ns): - if not Dispatcher.is_root(ns): - HostDispatcher.register_host(self) - super(HostDispatcher, self).__init__(name, bases, ns) - -class ContainerDispatcher(Dispatcher): - - def __init__(self, name, bases, ns): - if not Dispatcher.is_root(ns): - ContainerDispatcher.register_container(self) - ContainerDispatcher.register_commands(self) - super(ContainerDispatcher, self).__init__(name, bases, ns) diff --git a/src/command_system/framework.py b/src/command_system/framework.py index 2a6c7d7a82635bf0b949ca5e6a48fe1e64811142..9c2dd4cb0589656ccad9f469c0e9d664672f97da 100644 --- a/src/command_system/framework.py +++ b/src/command_system/framework.py @@ -23,7 +23,8 @@ import re from types import FunctionType from inspect import getargspec, getdoc -from dispatching import Dispatcher, HostDispatcher, ContainerDispatcher +from dispatcher import Host, Container +from dispatcher import get_command, list_commands from mapping import parse_arguments, adapt_arguments from errors import DefinitionError, CommandError, NoCommandError @@ -32,8 +33,12 @@ class CommandHost(object): Command host is a hub between numerous command processors and command containers. Aimed to participate in a dispatching process in order to provide clean and transparent architecture. + + 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__ = HostDispatcher + __metaclass__ = Host class CommandContainer(object): """ @@ -41,11 +46,15 @@ class CommandContainer(object): allowing them to be dispatched and proccessed correctly. Each command container may be bound to a one or more command hosts. - Bounding is controlled by the HOSTS variable, which must be defined - in the body of the command container. This variable should contain a - list of hosts to bound to, as a tuple or list. + 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__ = ContainerDispatcher + __metaclass__ = Container class CommandProcessor(object): """ @@ -126,13 +135,13 @@ class CommandProcessor(object): pass def get_command(self, name): - command = Dispatcher.get_command(self.COMMAND_HOST, name) + command = get_command(self.COMMAND_HOST, name) if not command: raise NoCommandError("Command does not exist", name=name) return command def list_commands(self): - commands = Dispatcher.list_commands(self.COMMAND_HOST) + commands = list_commands(self.COMMAND_HOST) commands = dict(commands) return sorted(set(commands.itervalues())) diff --git a/src/command_system/implementation/execute.py b/src/command_system/implementation/execute.py index c46ebb967c15a07e6f427a17b429465b211c2d4e..0e95c67bd66842b1f38d01c9dfc531ccb5def0d4 100644 --- a/src/command_system/implementation/execute.py +++ b/src/command_system/implementation/execute.py @@ -41,6 +41,7 @@ from ..framework import CommandContainer, command, doc from hosts import * class Execute(CommandContainer): + AUTOMATIC = True HOSTS = ChatCommands, PrivateChatCommands, GroupChatCommands DIRECTORY = "~" diff --git a/src/command_system/implementation/hosts.py b/src/command_system/implementation/hosts.py index 3624da2dca50684c1635563bb63faf4189e6999a..285372e437ee9c5647809aee471617c4b15cd0dc 100644 --- a/src/command_system/implementation/hosts.py +++ b/src/command_system/implementation/hosts.py @@ -25,18 +25,18 @@ class ChatCommands(CommandHost): This command host is bound to the command processor which processes commands from a chat. """ - pass + AUTOMATIC = True class PrivateChatCommands(CommandHost): """ This command host is bound to the command processor which processes commands from a private chat. """ - pass + AUTOMATIC = True class GroupChatCommands(CommandHost): """ This command host is bound to the command processor which processes commands from a group chat. """ - pass + AUTOMATIC = True diff --git a/src/command_system/implementation/standard.py b/src/command_system/implementation/standard.py index 05fa08dc2f8711c95ae4fa0bd18847323c353a69..dfbef1c7fa2edc1521c34049b48a37b069a0cc01 100644 --- a/src/command_system/implementation/standard.py +++ b/src/command_system/implementation/standard.py @@ -43,6 +43,7 @@ class StandardCommonCommands(CommandContainer): to all - chat, private chat, group chat. """ + AUTOMATIC = True HOSTS = ChatCommands, PrivateChatCommands, GroupChatCommands @command @@ -164,6 +165,7 @@ class StandardCommonChatCommands(CommandContainer): to a chat and a private chat only. """ + AUTOMATIC = True HOSTS = ChatCommands, PrivateChatCommands @command @@ -221,6 +223,7 @@ class StandardChatCommands(CommandContainer): to a chat. """ + AUTOMATIC = True HOSTS = (ChatCommands,) class StandardPrivateChatCommands(CommandContainer): @@ -229,6 +232,7 @@ class StandardPrivateChatCommands(CommandContainer): to a private chat. """ + AUTOMATIC = True HOSTS = (PrivateChatCommands,) class StandardGroupChatCommands(CommandContainer): @@ -237,6 +241,7 @@ class StandardGroupChatCommands(CommandContainer): to a group chat. """ + AUTOMATIC = True HOSTS = (GroupChatCommands,) @command(raw=True) diff --git a/src/command_system/tools.py b/src/command_system/tools.py new file mode 100644 index 0000000000000000000000000000000000000000..206e95a19d3278136720e33a47ae5ca1cc03de46 --- /dev/null +++ b/src/command_system/tools.py @@ -0,0 +1,35 @@ +# 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. + +from types import * + +def remove(sequence, target): + if isinstance(sequence, ListType): + if target in sequence: + sequence.remove(target) + elif isinstance(sequence, DictType): + if target in sequence: + del sequence[target] \ No newline at end of file