Getting start on this here em code
authorEd Page <eopage@byu.net>
Fri, 25 Sep 2009 22:10:33 +0000 (17:10 -0500)
committerEd Page <eopage@byu.net>
Fri, 25 Sep 2009 22:10:33 +0000 (17:10 -0500)
22 files changed:
src/aliasing.py [new file with mode: 0644]
src/avatars.py [new file with mode: 0644]
src/channel/__init__.py [new file with mode: 0644]
src/channel/contact_list.py [new file with mode: 0644]
src/channel/group.py [new file with mode: 0644]
src/channel/text.py [new file with mode: 0644]
src/channel_manager.py [new file with mode: 0644]
src/connection.py [new file with mode: 0644]
src/connection_manager.py [new file with mode: 0644]
src/handle.py [new file with mode: 0644]
src/presence.py [new file with mode: 0644]
src/simple_presence.py [new file with mode: 0644]
src/telepathy-theonering [new file with mode: 0755]
src/util/__init__.py [new file with mode: 0644]
src/util/algorithms.py [new file with mode: 0644]
src/util/concurrent.py [new file with mode: 0644]
src/util/coroutines.py [new file with mode: 0755]
src/util/go_utils.py [new file with mode: 0644]
src/util/io.py [new file with mode: 0644]
src/util/linux.py [new file with mode: 0644]
src/util/misc.py [new file with mode: 0644]
src/util/overloading.py [new file with mode: 0644]

diff --git a/src/aliasing.py b/src/aliasing.py
new file mode 100644 (file)
index 0000000..3f2cc25
--- /dev/null
@@ -0,0 +1,43 @@
+import logging
+
+import telepathy
+
+import handle
+
+
+class ButterflyAliasing(telepathy.server.ConnectionInterfaceAliasing):
+
+       def __init__(self):
+               telepathy.server.ConnectionInterfaceAliasing.__init__(self)
+
+       def GetAliasFlags(self):
+               return telepathy.constants.CONNECTION_ALIAS_FLAG_USER_SET
+
+       def RequestAliases(self, contacts):
+               logging.debug("Called RequestAliases")
+               return [self._get_alias(handleId) for handleId in contacts]
+
+       def GetAliases(self, contacts):
+               logging.debug("Called GetAliases")
+
+               result = {}
+               for contact in contacts:
+                       result[contact] = self._get_alias(contact)
+               return result
+
+       def SetAliases(self, aliases):
+               for handleId, alias in aliases.iteritems():
+                       h = self.handle(telepathy.HANDLE_TYPE_CONTACT, handleId)
+                       if h != handle.create_handle(self, 'self'):
+                               if alias == h.name:
+                                       alias = u""
+                               contact = h.contact
+                               if contact is None:
+                                       h.pending_alias = alias
+                                       continue
+                               infos = {}
+                               self.gvoice_client.update_contact_infos(contact, infos)
+                       else:
+                               self.gvoice_client.profile.display_name = alias.encode('utf-8')
+                               logging.info("Self alias changed to '%s'" % alias)
+                               self.AliasesChanged(((handle.create_handle(self, 'self'), alias), ))
diff --git a/src/avatars.py b/src/avatars.py
new file mode 100644 (file)
index 0000000..c623e1a
--- /dev/null
@@ -0,0 +1,62 @@
+import logging
+
+import telepathy
+
+
+class ButterflyAvatars(telepathy.server.ConnectionInterfaceAvatars):
+
+       def __init__(self):
+               self._avatar_known = False
+               telepathy.server.ConnectionInterfaceAvatars.__init__(self)
+
+       def GetAvatarRequirements(self):
+               mime_types = ("image/png","image/jpeg","image/gif")
+               return (mime_types, 96, 96, 192, 192, 500 * 1024)
+
+       def GetKnownAvatarTokens(self, contacts):
+               result = {}
+               for handle_id in contacts:
+                       handle = self.handle(telepathy.HANDLE_TYPE_CONTACT, handle_id)
+                       if handle == self.GetSelfHandle():
+                               contact = handle.profile
+                       else:
+                               contact = handle.contact
+
+                       if contact is not None:
+                               msn_object = contact.msn_object
+                       else:
+                               msn_object = None
+
+                       if msn_object is not None:
+                               result[handle] = msn_object._data_sha.encode("hex")
+                       elif self._avatar_known:
+                               result[handle] = ""
+               return result
+
+       def RequestAvatars(self, contacts):
+               for handle_id in contacts:
+                       handle = self.handle(telepathy.HANDLE_TYPE_CONTACT, handle_id)
+                       if handle == self.GetSelfHandle():
+                               msn_object = self.msn_client.profile.msn_object
+                               self._msn_object_retrieved(msn_object, handle)
+                       else:
+                               contact = handle.contact
+                               if contact is not None:
+                                       msn_object = contact.msn_object
+                               else:
+                                       msn_object = None
+                               if msn_object is not None:
+                                       self.msn_client.msn_object_store.request(msn_object,\
+                                                       (self._msn_object_retrieved, handle))
+
+       def SetAvatar(self, avatar, mime_type):
+               self._avatar_known = True
+               if not isinstance(avatar, str):
+                       avatar = "".join([chr(b) for b in avatar])
+               avatarToken = 0
+               logging.info("Setting self avatar to %s" % avatarToken)
+               return avatarToken
+
+       def ClearAvatar(self):
+               self.msn_client.profile.msn_object = None
+               self._avatar_known = True
diff --git a/src/channel/__init__.py b/src/channel/__init__.py
new file mode 100644 (file)
index 0000000..de25e95
--- /dev/null
@@ -0,0 +1,5 @@
+#!/usr/bin/env python
+
+import group
+import contact_list
+import text
diff --git a/src/channel/contact_list.py b/src/channel/contact_list.py
new file mode 100644 (file)
index 0000000..fb80289
--- /dev/null
@@ -0,0 +1,111 @@
+import weakref
+import logging
+
+import telepathy
+
+import handle
+
+
+def create_contact_list_channel(connection, h):
+       if h.get_name() == 'subscribe':
+               channel_class = TheOneRingSubscribeListChannel
+       elif h.get_name() == 'publish':
+               channel_class = TheOneRingPublishListChannel
+       elif h.get_name() == 'hide':
+               logging.warn("Unsuported type %s" % h.get_name())
+       elif h.get_name() == 'allow':
+               logging.warn("Unsuported type %s" % h.get_name())
+       elif h.get_name() == 'deny':
+               logging.warn("Unsuported type %s" % h.get_name())
+       else:
+               raise TypeError("Unknown list type : " + h.get_name())
+       return channel_class(connection, h)
+
+
+class TheOneRingListChannel(
+               telepathy.server.ChannelTypeContactList,
+               telepathy.server.ChannelInterfaceGroup,
+       ):
+       "Abstract Contact List channels"
+
+       def __init__(self, connection, h):
+               telepathy.server.ChannelTypeContactList.__init__(self, connection, h)
+               telepathy.server.ChannelInterfaceGroup.__init__(self)
+
+               self._conn_ref = weakref.ref(connection)
+
+       def GetLocalPendingMembersWithInfo(self):
+               return []
+
+
+class TheOneRingSubscribeListChannel(TheOneRingListChannel):
+       """
+       Subscribe List channel.
+
+       This channel contains the list of contact to whom the current used is
+       'subscribed', basically this list contains the contact for whom you are
+       supposed to receive presence notification.
+       """
+
+       def __init__(self, connection, h):
+               TheOneRingListChannel.__init__(self, connection, h)
+               self.GroupFlagsChanged(
+                       telepathy.CHANNEL_GROUP_FLAG_CAN_ADD |
+                       telepathy.CHANNEL_GROUP_FLAG_CAN_REMOVE,
+                       0,
+               )
+
+       def AddMembers(self, contacts, message):
+               addressBook = self._conn.gvoice_client
+               for h in contacts:
+                       h = self._conn.handle(telepathy.HANDLE_TYPE_CONTACT, h)
+                       contact = h.contact
+                       if contact is None:
+                               account = h.account
+                       else:
+                               account = contact.account
+                       groups = list(h.pending_groups)
+                       h.pending_groups = set()
+                       addressBook.add_messenger_contact(account,
+                                       invite_message=message.encode('utf-8'),
+                                       groups=groups)
+
+       def RemoveMembers(self, contacts, message):
+               addressBook = self._conn.gvoice_client
+               for h in contacts:
+                       h = self._conn.handle(telepathy.HANDLE_TYPE_CONTACT, h)
+                       contact = h.contact
+                       addressBook.delete_contact(contact)
+
+
+class TheOneRingPublishListChannel(TheOneRingListChannel):
+
+       def __init__(self, connection, h):
+               TheOneRingListChannel.__init__(self, connection, h)
+               self.GroupFlagsChanged(0, 0)
+
+       def AddMembers(self, contacts, message):
+               addressBook = self._conn.gvoice_client
+               for contactHandleId in contacts:
+                       contactHandle = self._conn.handle(telepathy.HANDLE_TYPE_CONTACT,
+                                               contactHandleId)
+                       contact = contactHandle.contact
+                       addressBook.accept_contact_invitation(contact, False)
+
+       def RemoveMembers(self, contacts, message):
+               addressBook = self._conn.gvoice_client
+               for contactHandleId in contacts:
+                       contactHandle = self._conn.handle(telepathy.HANDLE_TYPE_CONTACT,
+                                               contactHandleId)
+                       contact = contactHandle.contact
+
+       def GetLocalPendingMembersWithInfo(self):
+               addressBook = self._conn.gvoice_client
+               result = []
+               for contact in addressBook.contacts:
+                       h = handle.create_handle(self._conn_ref(), 'contact',
+                                       contact.account, contact.network_id)
+                       result.append((h, h,
+                                       telepathy.CHANNEL_GROUP_CHANGE_REASON_INVITED,
+                                       contact.attributes.get('invite_message', '')))
+               return result
diff --git a/src/channel/group.py b/src/channel/group.py
new file mode 100644 (file)
index 0000000..f67f78c
--- /dev/null
@@ -0,0 +1,67 @@
+import logging
+
+import telepathy
+
+import contact_list
+
+
+class TheOneRingGroupChannel(contact_list.TheOneRingListChannel):
+
+       def __init__(self, connection, h):
+               self.__pending_add = []
+               self.__pending_remove = []
+               contact_list.TheOneRingListChannel.__init__(self, connection, h)
+               self.GroupFlagsChanged(
+                       telepathy.CHANNEL_GROUP_FLAG_CAN_ADD | telepathy.CHANNEL_GROUP_FLAG_CAN_REMOVE,
+                       0,
+               )
+
+       def AddMembers(self, contacts, message):
+               addressBook = self._conn.gvoice_client
+               if self._handle.group is None:
+                       for contactHandleId in contacts:
+                               contactHandle = self._conn.handle(telepathy.HANDLE_TYPE_CONTACT, contactHandleId)
+                               logging.info("Adding contact %r to pending group %r" % (contactHandle, self._handle))
+                               if contactHandleId in self.__pending_remove:
+                                       self.__pending_remove.remove(contactHandleId)
+                               else:
+                                       self.__pending_add.append(contactHandleId)
+                       return
+               else:
+                       for contactHandleId in contacts:
+                               contactHandle = self._conn.handle(telepathy.HANDLE_TYPE_CONTACT, contactHandleId)
+                               logging.info("Adding contact %r to group %r" % (contactHandle, self._handle))
+                               contact = contactHandle.contact
+                               group = self._handle.group
+                               if contact is not None:
+                                       addressBook.add_contact_to_group(group, contact)
+                               else:
+                                       contactHandle.pending_groups.add(group)
+
+       def RemoveMembers(self, contacts, message):
+               addressBook = self._conn.gvoice_client
+               if self._handle.group is None:
+                       for contactHandleId in contacts:
+                               contactHandle = self._conn.handle(telepathy.HANDLE_TYPE_CONTACT, contactHandleId)
+                               logging.info("Adding contact %r to pending group %r" % (contactHandle, self._handle))
+                               if contactHandleId in self.__pending_add:
+                                       self.__pending_add.remove(contactHandleId)
+                               else:
+                                       self.__pending_remove.append(contactHandleId)
+                       return
+               else:
+                       for contactHandleId in contacts:
+                               contactHandle = self._conn.handle(telepathy.HANDLE_TYPE_CONTACT, contactHandleId)
+                               logging.info("Removing contact %r from pending group %r" % (contactHandle, self._handle))
+                               contact = contactHandle.contact
+                               group = self._handle.group
+                               if contact is not None:
+                                       addressBook.delete_contact_from_group(group, contact)
+                               else:
+                                       contactHandle.pending_groups.discard(group)
+
+       def Close(self):
+               logging.debug("Deleting group %s" % self._handle.name)
+               addressBook = self._conn.gvoice_client
+               group = self._handle.group
+               addressBook.delete_group(group)
diff --git a/src/channel/text.py b/src/channel/text.py
new file mode 100644 (file)
index 0000000..e6cc6af
--- /dev/null
@@ -0,0 +1,45 @@
+import time
+import weakref
+
+import telepathy
+
+import handle
+
+
+class ButterflyTextChannel(
+               telepathy.server.ChannelTypeText,
+               telepathy.server.ChannelInterfaceGroup,
+               telepathy.server.ChannelInterfaceChatState
+       ):
+
+       def __init__(self, connection, conversation):
+               self._recv_id = 0
+               self._conversation = conversation
+               self._conn_ref = weakref.ref(connection)
+
+               telepathy.server.ChannelTypeText.__init__(self, connection, None)
+               telepathy.server.ChannelInterfaceGroup.__init__(self)
+               telepathy.server.ChannelInterfaceChatState.__init__(self)
+
+               self.GroupFlagsChanged(telepathy.CHANNEL_GROUP_FLAG_CAN_ADD, 0)
+               self.__add_initial_participants()
+
+       def SetChatState(self, state):
+               if state == telepathy.CHANNEL_CHAT_STATE_COMPOSING:
+                       self._conversation.send_typing_notification()
+               h = handle.create_handle(self._conn_ref(), 'self')
+               self.ChatStateChanged(h, state)
+
+       def Send(self, messageType, text):
+               if messageType == telepathy.CHANNEL_TEXT_MESSAGE_TYPE_NORMAL:
+                       self._conversation.send_text_message(text)
+               elif messageType == telepathy.CHANNEL_TEXT_MESSAGE_TYPE_ACTION and text == u"nudge":
+                       self._conversation.send_nudge()
+               else:
+                       raise telepathy.NotImplemented("Unhandled message type")
+               self.Sent(int(time.time()), messageType, text)
+
+       def Close(self):
+               self._conversation.leave()
+               telepathy.server.ChannelTypeText.Close(self)
+               self.remove_from_connection()
diff --git a/src/channel_manager.py b/src/channel_manager.py
new file mode 100644 (file)
index 0000000..ba06957
--- /dev/null
@@ -0,0 +1,49 @@
+import weakref
+import logging
+
+import telepathy
+
+import channel
+
+
+class ChannelManager(object):
+
+       def __init__(self, connection):
+               self._connRef = weakref.ref(connection)
+               self._listChannels = weakref.WeakValueDictionary()
+               self._textChannels = weakref.WeakValueDictionary()
+
+       def close(self):
+               for chan in self._listChannels.values():
+                       chan.remove_from_connection()# so that dbus lets it die.
+               for chan in self._textChannels.values():
+                       chan.Close()
+
+       def channel_for_list(self, handle, suppress_handler=False):
+               if handle in self._listChannels:
+                       chan = self._listChannels[handle]
+               else:
+                       if handle.get_type() == telepathy.HANDLE_TYPE_GROUP:
+                               chan = channel.group.GroupChannel(self._connRef(), handle)
+                       elif handle.get_type() == telepathy.HANDLE_TYPE_CONTACT:
+                               chan = channel.contact_list.creat_contact_list_channel(self._connRef(), handle)
+                       else:
+                               logging.warn("Unknown channel type %r" % handle.get_type())
+                       self._listChannels[handle] = chan
+                       self._connRef().add_channel(chan, handle, suppress_handler)
+               return chan
+
+       def channel_for_text(self, handle, conversation=None, suppress_handler=False):
+               if handle in self._textChannels:
+                       chan = self._textChannels[handle]
+               else:
+                       logging.debug("Requesting new text channel")
+                       contact = handle.contact
+
+                       if conversation is None:
+                               client = self._connRef().msn_client
+                               conversation = None
+                       chan = channel.text.TextChannel(self._connRef(), conversation)
+                       self._textChannels[handle] = chan
+                       self._connRef().add_channel(chan, handle, suppress_handler)
+               return chan
diff --git a/src/connection.py b/src/connection.py
new file mode 100644 (file)
index 0000000..2dce28a
--- /dev/null
@@ -0,0 +1,140 @@
+import weakref
+import logging
+
+import telepathy
+
+import constants
+import gv_backend
+import handle
+import channel_manager
+
+
+class TheOneRingConnection(telepathy.server.Connection):
+
+       _mandatory_parameters = {
+               'account' : 's',
+               'password' : 's'
+       }
+
+       def __init__(self, manager, parameters):
+               try:
+                       self.check_parameters(parameters)
+                       account = unicode(parameters['account'])
+
+                       telepathy.server.Connection.__init__(self, 'gvoice', account, 'theonering')
+
+                       self._manager = weakref.proxy(manager)
+                       self._credentials = (
+                               parameters['account'].encode('utf-8'),
+                               parameters['password'].encode('utf-8'),
+                       )
+                       self._channelManager = channel_manager.ChannelManager(self)
+
+                       cookieFilePath = "%s/cookies.txt" % constants._data_path_
+                       self._backend = gv_backend.GVDialer(cookieFilePath)
+
+                       self.set_self_handle(handle.create_handle(self, 'self'))
+
+                       self.__disconnect_reason = telepathy.CONNECTION_STATUS_REASON_NONE_SPECIFIED
+                       self._initial_presence = None
+                       self._initial_personal_message = None
+
+                       logging.info("Connection to the account %s created" % account)
+               except Exception, e:
+                       logging.exception("Failed to create Connection")
+                       raise
+
+       @property
+       def manager(self):
+               return self._manager
+
+       @property
+       def gvoice_backend(self):
+               return self._backend
+
+       @property
+       def username(self):
+               self._credentials[0]
+
+       def handle(self, handleType, handleId):
+               self.check_handle(handleType, handleId)
+               return self._handles[handleType, handleId]
+
+       def Connect(self):
+               """
+               org.freedesktop.telepathy.Connection
+               """
+               logging.info("Connecting")
+               self.__disconnect_reason = telepathy.CONNECTION_STATUS_REASON_NONE_SPECIFIED
+               self._backend.login(*self._credentials)
+
+       def Disconnect(self):
+               """
+               org.freedesktop.telepathy.Connection
+               """
+               logging.info("Disconnecting")
+               self.__disconnect_reason = telepathy.CONNECTION_STATUS_REASON_REQUESTED
+               self._backend.logout()
+
+       def RequestChannel(self, type, handleType, handleId, suppressHandler):
+               """
+               org.freedesktop.telepathy.Connection
+               @param type DBus interface name for base channel type
+               @param handleId represents a contact, list, etc according to handleType
+
+               @returns DBus object path for the channel created or retrieved
+               """
+               self.check_connected()
+
+               channel = None
+               channelManager = self._channelManager
+               handle = self.handle(handleType, handleId)
+
+               if type == telepathy.CHANNEL_TYPE_CONTACT_LIST:
+                       channel = channelManager.channel_for_list(handle, suppressHandler)
+               elif type == telepathy.CHANNEL_TYPE_TEXT:
+                       if handleType != telepathy.HANDLE_TYPE_CONTACT:
+                               raise telepathy.NotImplemented("Only Contacts are allowed")
+                       contact = handle.contact
+                       channel = channelManager.channel_for_text(handle, None, suppressHandler)
+               else:
+                       raise telepathy.NotImplemented("unknown channel type %s" % type)
+
+               return channel._object_path
+
+       def RequestHandles(self, handleType, names, sender):
+               """
+               org.freedesktop.telepathy.Connection
+               """
+               self.check_connected()
+               self.check_handleType(handleType)
+
+               handles = []
+               for name in names:
+                       name = name.encode('utf-8')
+                       if handleType == telepathy.HANDLE_TYPE_CONTACT:
+                               h = self._create_contact_handle(name)
+                       elif handleType == telepathy.HANDLE_TYPE_LIST:
+                               h = handle.create_handle(self, 'list', name)
+                       elif handleType == telepathy.HANDLE_TYPE_GROUP:
+                               h = handle.create_handle(self, 'group', name)
+                       else:
+                               raise telepathy.NotAvailable('Handle type unsupported %d' % handleType)
+                       handles.append(h.id)
+                       self.add_client_handle(handle, sender)
+               return handles
+
+       def _create_contact_handle(self, name):
+               requestedContactId, requestedContactName = handle.field_split(name)
+
+               contacts = self._backend.get_contacts()
+               contactsFound = [
+                       (contactId, contactName) for (contactId, contactName) in contacts
+                       if contactName == name
+               ]
+
+               if 0 < len(contactsFound):
+                       contactId, contactName = contactsFound[0]
+                       h = handle.create_handle(self, 'contact', contactId, contactName)
+               else:
+                       h = handle.create_handle(self, 'contact', requestedContactId, requestedContactName)
diff --git a/src/connection_manager.py b/src/connection_manager.py
new file mode 100644 (file)
index 0000000..dfcb91a
--- /dev/null
@@ -0,0 +1,74 @@
+import logging
+
+import gobject
+import telepathy
+
+import connection
+
+
+class TheOneRingConnectionManager(telepathy.server.ConnectionManager):
+
+       def __init__(self, shutdown_func=None):
+               telepathy.server.ConnectionManager.__init__(self, 'theonering')
+
+               self._protos['gvoice'] = connection.TheOneRingConnection
+               self._on_shutdown = shutdown_func
+               logging.info("Connection manager created")
+
+       def GetParameters(self, proto):
+               """
+               org.freedesktop.telepathy.ConnectionManager
+
+               @returns the mandatory and optional parameters for creating a connection
+               """
+               if proto not in self._protos:
+                       raise telepathy.NotImplemented('unknown protocol %s' % proto)
+
+               result = []
+               connection_class = self._protos[proto]
+               mandatory_parameters = connection_class._mandatory_parameters
+               optional_parameters = connection_class._optional_parameters
+               default_parameters = connection_class._parameter_defaults
+
+               for parameter_name, parameter_type in mandatory_parameters.iteritems():
+                       param = (
+                               parameter_name,
+                               telepathy.CONN_MGR_PARAM_FLAG_REQUIRED,
+                               parameter_type,
+                               '',
+                       )
+                       result.append(param)
+
+               for parameter_name, parameter_type in optional_parameters.iteritems():
+                       if parameter_name in default_parameters:
+                               param = (
+                                       parameter_name,
+                                       telepathy.CONN_MGR_PARAM_FLAG_HAS_DEFAULT,
+                                       parameter_name,
+                                       default_parameters[parameter_name],
+                               )
+                       else:
+                               param = (parameter_name, 0, parameter_name, '')
+                       result.append(param)
+
+               return result
+
+       def disconnected(self, conn):
+               result = telepathy.server.ConnectionManager.disconnected(self, conn)
+               gobject.timeout_add(5000, self.shutdown)
+
+       def quit(self):
+               """
+               Terminates all connections. Must be called upon quit
+               """
+               for connection in self._connections:
+                       connection.Disconnect()
+               logging.info("Connection manager quitting")
+
+       def _shutdown(self):
+               if (
+                       self._on_shutdown is not None and
+                       len(self._connections) == 0
+               ):
+                       self._on_shutdown()
+               return False
diff --git a/src/handle.py b/src/handle.py
new file mode 100644 (file)
index 0000000..022a093
--- /dev/null
@@ -0,0 +1,122 @@
+import logging
+import weakref
+
+import telepathy
+
+
+class MetaMemoize(type):
+       """
+       Allows a class to cache off instances for reuse
+       """
+
+       def __call__(cls, connection, *args):
+               obj, newlyCreated = cls.__new__(cls, connection, *args)
+               if newlyCreated:
+                       obj.__init__(connection, connection.get_handle_id(), *args)
+                       logging.info("New Handle %r" % obj)
+               return obj
+
+
+class TheOneRingHandle(telepathy.server.Handle):
+       """
+       Instances are memoized
+       """
+
+       __metaclass__ = MetaMemoize
+
+       _instances = weakref.WeakValueDictionary()
+
+       def __new__(cls, connection, *args):
+               key = cls, connection.username, args
+               if key in cls._instances.keys():
+                       return cls._instances[key], False
+               else:
+                       instance = object.__new__(cls, connection, *args)
+                       cls._instances[key] = instance # TRICKY: instances is a weakdict
+                       return instance, True
+
+       def __init__(self, connection, id, handleType, name):
+               telepathy.server.Handle.__init__(self, id, handleType, name)
+               self._conn = weakref.proxy(connection)
+
+       def __repr__(self):
+               return "<%s id=%u name='%s'>" % (
+                       type(self).__name__, self.id, self.name
+               )
+
+       id = property(telepathy.server.Handle.get_id)
+       type = property(telepathy.server.Handle.get_type)
+       name = property(telepathy.server.Handle.get_name)
+
+
+class SelfHandle(TheOneRingHandle):
+
+       instance = None
+
+       def __init__(self, connection, id):
+               handleType = telepathy.HANDLE_TYPE_CONTACT
+               handleName = connection.username
+               TheOneRingHandle.__init__(self, connection, id, handleType, handleName)
+
+               self.profile = connection.username
+
+
+def field_join(fields):
+       """
+       >>> field_join("1", "First Name")
+       '1#First Name'
+       """
+       return "#".join(fields)
+
+
+def field_split(fields):
+       """
+       >>> field_split('1#First Name')
+       ['1', 'First Name']
+       """
+       return fields.split("#")
+
+
+class ContactHandle(TheOneRingHandle):
+
+       def __init__(self, connection, id, contactId, contactAccount):
+               handleType = telepathy.HANDLE_TYPE_CONTACT
+               handleName = field_join(contactId, contactAccount)
+               TheOneRingHandle.__init__(self, connection, id, handleType, handleName)
+
+               self.account = contactAccount
+               self._id = contactId
+
+       @property
+       def contact(self):
+               return self._conn.gvoice_client.get_contact_details(self._id)
+
+
+class ListHandle(TheOneRingHandle):
+
+       def __init__(self, connection, id, listName):
+               handleType = telepathy.HANDLE_TYPE_LIST
+               handleName = listName
+               TheOneRingHandle.__init__(self, connection, id, handleType, handleName)
+
+
+class GroupHandle(TheOneRingHandle):
+
+       def __init__(self, connection, id, groupName):
+               handleType = telepathy.HANDLE_TYPE_GROUP
+               handleName = groupName
+               TheOneRingHandle.__init__(self, connection, id, handleType, handleName)
+
+
+_HANDLE_TYPE_MAPPING = {
+       'self': SelfHandle,
+       'contact': ContactHandle,
+       'list': ListHandle,
+       'group': GroupHandle,
+}
+
+
+def create_handle(connection, type, *args):
+       handle = _HANDLE_TYPE_MAPPING[type](connection, *args)
+       connection._handles[handle.get_type(), handle.get_id()] = handle
+       return handle
diff --git a/src/presence.py b/src/presence.py
new file mode 100644 (file)
index 0000000..8350e71
--- /dev/null
@@ -0,0 +1,95 @@
+import logging
+
+import telepathy
+
+import simple_presence
+
+
+class ButterflyPresence(telepathy.server.ConnectionInterfacePresence):
+
+       def __init__(self):
+               telepathy.server.ConnectionInterfacePresence.__init__(self)
+
+       def GetStatuses(self):
+               # the arguments are in common to all on-line presences
+               arguments = {'message' : 's'}
+
+               # you get one of these for each status
+               # {name:(type, self, exclusive, {argument:types}}
+               return {
+                       simple_presence.ButterflyPresenceMapping.ONLINE:(
+                               telepathy.CONNECTION_PRESENCE_TYPE_AVAILABLE,
+                               True, True, arguments),
+                       simple_presence.ButterflyPresenceMapping.AWAY:(
+                               telepathy.CONNECTION_PRESENCE_TYPE_AWAY,
+                               True, True, arguments),
+                       simple_presence.ButterflyPresenceMapping.BUSY:(
+                               telepathy.CONNECTION_PRESENCE_TYPE_AWAY,
+                               True, True, arguments),
+                       simple_presence.ButterflyPresenceMapping.IDLE:(
+                               telepathy.CONNECTION_PRESENCE_TYPE_EXTENDED_AWAY,
+                               True, True, arguments),
+                       simple_presence.ButterflyPresenceMapping.BRB:(
+                               telepathy.CONNECTION_PRESENCE_TYPE_AWAY,
+                               True, True, arguments),
+                       simple_presence.ButterflyPresenceMapping.PHONE:(
+                               telepathy.CONNECTION_PRESENCE_TYPE_AWAY,
+                               True, True, arguments),
+                       simple_presence.ButterflyPresenceMapping.LUNCH:(
+                               telepathy.CONNECTION_PRESENCE_TYPE_EXTENDED_AWAY,
+                               True, True, arguments),
+                       simple_presence.ButterflyPresenceMapping.INVISIBLE:(
+                               telepathy.CONNECTION_PRESENCE_TYPE_HIDDEN,
+                               True, True, {}),
+                       simple_presence.ButterflyPresenceMapping.OFFLINE:(
+                               telepathy.CONNECTION_PRESENCE_TYPE_OFFLINE,
+                               True, True, {})
+               }
+
+       def RequestPresence(self, contacts):
+               presences = self.get_presences(contacts)
+               self.PresenceUpdate(presences)
+
+       def GetPresence(self, contacts):
+               return self.get_presences(contacts)
+
+       def SetStatus(self, statuses):
+               status, arguments = statuses.items()[0]
+               if status == simple_presence.ButterflyPresenceMapping.OFFLINE:
+                       self.Disconnect()
+
+               presence = simple_presence.ButterflyPresenceMapping.to_pymsn[status]
+               message = arguments.get('message', u'').encode("utf-8")
+
+               logging.info("Setting Presence to '%s'" % presence)
+               logging.info("Setting Personal message to '%s'" % message)
+
+               if self._status != telepathy.CONNECTION_STATUS_CONNECTED:
+                       self._initial_presence = presence
+                       self._initial_personal_message = message
+               else:
+                       self.msn_client.profile.personal_message = message
+                       self.msn_client.profile.presence = presence
+
+       def get_presences(self, contacts):
+               presences = {}
+               for handleId in contacts:
+                       h = self.handle(telepathy.HANDLE_TYPE_CONTACT, handleId)
+                       try:
+                               contact = h.contact
+                       except AttributeError:
+                               contact = h.profile
+
+                       if contact is not None:
+                               presence = simple_presence.ButterflyPresenceMapping.to_telepathy[contact.presence]
+                               personal_message = unicode(contact.personal_message, "utf-8")
+                       else:
+                               presence = simple_presence.ButterflyPresenceMapping.OFFLINE
+                               personal_message = u""
+
+                       arguments = {}
+                       if personal_message:
+                               arguments = {'message' : personal_message}
+
+                       presences[h] = (0, {presence : arguments}) # TODO: Timestamp
+               return presences
diff --git a/src/simple_presence.py b/src/simple_presence.py
new file mode 100644 (file)
index 0000000..4f1c09b
--- /dev/null
@@ -0,0 +1,139 @@
+import logging
+
+import telepathy
+
+
+class ButterflyPresenceMapping(object):
+       ONLINE = 'available'
+       AWAY = 'away'
+       BUSY = 'dnd'
+       IDLE = 'xa'
+       BRB = 'brb'
+       PHONE = 'phone'
+       LUNCH = 'lunch'
+       INVISIBLE = 'hidden'
+       OFFLINE = 'offline'
+
+       to_pymsn = {
+               ONLINE: pymsn.Presence.ONLINE,
+               AWAY: pymsn.Presence.AWAY,
+               BUSY: pymsn.Presence.BUSY,
+               IDLE: pymsn.Presence.IDLE,
+               BRB: pymsn.Presence.BE_RIGHT_BACK,
+               PHONE: pymsn.Presence.ON_THE_PHONE,
+               LUNCH: pymsn.Presence.OUT_TO_LUNCH,
+               INVISIBLE: pymsn.Presence.INVISIBLE,
+               OFFLINE: pymsn.Presence.OFFLINE
+       }
+
+       to_telepathy = {
+               pymsn.Presence.ONLINE: ONLINE,
+               pymsn.Presence.AWAY: AWAY,
+               pymsn.Presence.BUSY: BUSY,
+               pymsn.Presence.IDLE: IDLE,
+               pymsn.Presence.BE_RIGHT_BACK: BRB,
+               pymsn.Presence.ON_THE_PHONE: PHONE,
+               pymsn.Presence.OUT_TO_LUNCH: LUNCH,
+               pymsn.Presence.INVISIBLE: INVISIBLE,
+               pymsn.Presence.OFFLINE: OFFLINE
+       }
+
+       to_presence_type = {
+               ONLINE: telepathy.constants.CONNECTION_PRESENCE_TYPE_AVAILABLE,
+               AWAY: telepathy.constants.CONNECTION_PRESENCE_TYPE_AWAY,
+               BUSY: telepathy.constants.CONNECTION_PRESENCE_TYPE_BUSY,
+               IDLE: telepathy.constants.CONNECTION_PRESENCE_TYPE_EXTENDED_AWAY,
+               BRB: telepathy.constants.CONNECTION_PRESENCE_TYPE_AWAY,
+               PHONE: telepathy.constants.CONNECTION_PRESENCE_TYPE_BUSY,
+               LUNCH: telepathy.constants.CONNECTION_PRESENCE_TYPE_EXTENDED_AWAY,
+               INVISIBLE: telepathy.constants.CONNECTION_PRESENCE_TYPE_HIDDEN,
+               OFFLINE: telepathy.constants.CONNECTION_PRESENCE_TYPE_OFFLINE
+       }
+
+
+class ButterflySimplePresence(telepathy.server.ConnectionInterfaceSimplePresence):
+
+       def __init__(self):
+               telepathy.server.ConnectionInterfaceSimplePresence.__init__(self)
+
+               dbus_interface = 'org.freedesktop.Telepathy.Connection.Interface.SimplePresence'
+
+               self._implement_property_get(dbus_interface, {'Statuses' : self.get_statuses})
+
+       def GetPresences(self, contacts):
+               return self.get_simple_presences(contacts)
+
+       def SetPresence(self, status, message):
+               if status == ButterflyPresenceMapping.OFFLINE:
+                       self.Disconnect()
+
+               try:
+                       presence = ButterflyPresenceMapping.to_pymsn[status]
+               except KeyError:
+                       raise telepathy.errors.InvalidArgument
+               message = message.encode("utf-8")
+
+               logging.info("Setting Presence to '%s'" % presence)
+               logging.info("Setting Personal message to '%s'" % message)
+
+               if self._status != telepathy.CONNECTION_STATUS_CONNECTED:
+                       self._initial_presence = presence
+                       self._initial_personal_message = message
+               else:
+                       self.msn_client.profile.personal_message = message
+                       self.msn_client.profile.presence = presence
+
+       def get_simple_presences(self, contacts):
+               presences = {}
+               for handle_id in contacts:
+                       handle = self.handle(telepathy.HANDLE_TYPE_CONTACT, handle_id)
+                       try:
+                               contact = handle.contact
+                       except AttributeError:
+                               contact = handle.profile
+
+                       if contact is not None:
+                               presence = ButterflyPresenceMapping.to_telepathy[contact.presence]
+                               personal_message = unicode(contact.personal_message, "utf-8")
+                       else:
+                               presence = ButterflyPresenceMapping.OFFLINE
+                               personal_message = u""
+
+                       presence_type = ButterflyPresenceMapping.to_presence_type[presence]
+
+                       presences[handle] = (presence_type, presence, personal_message)
+               return presences
+
+       def get_statuses(self):
+               # you get one of these for each status
+               # {name:(Type, May_Set_On_Self, Can_Have_Message}
+               return {
+                       ButterflyPresenceMapping.ONLINE:(
+                               telepathy.CONNECTION_PRESENCE_TYPE_AVAILABLE,
+                               True, True),
+                       ButterflyPresenceMapping.AWAY:(
+                               telepathy.CONNECTION_PRESENCE_TYPE_AWAY,
+                               True, True),
+                       ButterflyPresenceMapping.BUSY:(
+                               telepathy.CONNECTION_PRESENCE_TYPE_AWAY,
+                               True, True),
+                       ButterflyPresenceMapping.IDLE:(
+                               telepathy.CONNECTION_PRESENCE_TYPE_EXTENDED_AWAY,
+                               True, True),
+                       ButterflyPresenceMapping.BRB:(
+                               telepathy.CONNECTION_PRESENCE_TYPE_AWAY,
+                               True, True),
+                       ButterflyPresenceMapping.PHONE:(
+                               telepathy.CONNECTION_PRESENCE_TYPE_AWAY,
+                               True, True),
+                       ButterflyPresenceMapping.LUNCH:(
+                               telepathy.CONNECTION_PRESENCE_TYPE_EXTENDED_AWAY,
+                               True, True),
+                       ButterflyPresenceMapping.INVISIBLE:(
+                               telepathy.CONNECTION_PRESENCE_TYPE_HIDDEN,
+                               True, False),
+                       ButterflyPresenceMapping.OFFLINE:(
+                               telepathy.CONNECTION_PRESENCE_TYPE_OFFLINE,
+                               True, False)
+               }
+
diff --git a/src/telepathy-theonering b/src/telepathy-theonering
new file mode 100755 (executable)
index 0000000..8f25a85
--- /dev/null
@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+
+"""
+Telepathy-TheOneRing - Telepathy plugin for GoogleVoice
+Copyright (C) 2009  Ed Page eopage AT byu DOT net
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library 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
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+"""
+
+import os
+import sys
+import signal
+import logging
+import gobject
+
+import dbus.glib
+import telepathy.utils as telepathy_utils
+
+import util.linux as linux_utils
+import util.go_utils as gobject_utils
+import constants
+from butterfly import ButterflyConnectionManager
+
+
+IDLE_TIMEOUT = 5000
+
+
+def run_theonering():
+       linux_utils.set_process_name(constants.__app_name__)
+
+       @gobject_utils.async
+       def quit():
+               manager.quit()
+               mainloop.quit()
+
+
+       if 'BUTTERFLY_PERSIST' not in os.environ:
+               def timeout_cb():
+                       if len(manager._connections) == 0:
+                               logging.info('No connection received - quitting')
+                               quit()
+                       return False
+               gobject.timeout_add(IDLE_TIMEOUT, timeout_cb)
+               shutdown_callback = quit
+       else:
+               shutdown_callback = None
+
+       signal.signal(signal.SIGTERM, quit)
+
+       try:
+               manager = ButterflyConnectionManager(shutdown_func=shutdown_callback)
+       except dbus.exceptions.NameExistsException:
+               logging.warning('Failed to acquire bus name, connection manager already running?')
+               sys.exit(1)
+
+       mainloop = gobject.MainLoop(is_running=True)
+
+       while mainloop.is_running():
+               try:
+                       mainloop.run()
+               except KeyboardInterrupt:
+                       quit()
+
+
+if __name__ == '__main__':
+       telepathy_utils.debug_divert_messages(os.getenv('BUTTERFLY_LOGFILE'))
+       logging.basicConfig(level=logging.DEBUG)
+       run_theonering()
diff --git a/src/util/__init__.py b/src/util/__init__.py
new file mode 100644 (file)
index 0000000..4265cc3
--- /dev/null
@@ -0,0 +1 @@
+#!/usr/bin/env python
diff --git a/src/util/algorithms.py b/src/util/algorithms.py
new file mode 100644 (file)
index 0000000..5da8b80
--- /dev/null
@@ -0,0 +1,490 @@
+#!/usr/bin/env python
+
+"""
+@note Source http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66448
+"""
+
+import itertools
+import functools
+import datetime
+import types
+
+
+def ordered_itr(collection):
+       """
+       >>> [v for v in ordered_itr({"a": 1, "b": 2})]
+       [('a', 1), ('b', 2)]
+       >>> [v for v in ordered_itr([3, 1, 10, -20])]
+       [-20, 1, 3, 10]
+       """
+       if isinstance(collection, types.DictType):
+               keys = list(collection.iterkeys())
+               keys.sort()
+               for key in keys:
+                       yield key, collection[key]
+       else:
+               values = list(collection)
+               values.sort()
+               for value in values:
+                       yield value
+
+
+def itercat(*iterators):
+       """
+       Concatenate several iterators into one.
+
+       >>> [v for v in itercat([1, 2, 3], [4, 1, 3])]
+       [1, 2, 3, 4, 1, 3]
+       """
+       for i in iterators:
+               for x in i:
+                       yield x
+
+
+def iterwhile(func, iterator):
+       """
+       Iterate for as long as func(value) returns true.
+       >>> through = lambda b: b
+       >>> [v for v in iterwhile(through, [True, True, False])]
+       [True, True]
+       """
+       iterator = iter(iterator)
+       while 1:
+               next = iterator.next()
+               if not func(next):
+                       raise StopIteration
+               yield next
+
+
+def iterfirst(iterator, count=1):
+       """
+       Iterate through 'count' first values.
+
+       >>> [v for v in iterfirst([1, 2, 3, 4, 5], 3)]
+       [1, 2, 3]
+       """
+       iterator = iter(iterator)
+       for i in xrange(count):
+               yield iterator.next()
+
+
+def iterstep(iterator, n):
+       """
+       Iterate every nth value.
+
+       >>> [v for v in iterstep([1, 2, 3, 4, 5], 1)]
+       [1, 2, 3, 4, 5]
+       >>> [v for v in iterstep([1, 2, 3, 4, 5], 2)]
+       [1, 3, 5]
+       >>> [v for v in iterstep([1, 2, 3, 4, 5], 3)]
+       [1, 4]
+       """
+       iterator = iter(iterator)
+       while True:
+               yield iterator.next()
+               # skip n-1 values
+               for dummy in xrange(n-1):
+                       iterator.next()
+
+
+def itergroup(iterator, count, padValue = None):
+       """
+       Iterate in groups of 'count' values. If there
+       aren't enough values, the last result is padded with
+       None.
+
+       >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
+       ...     print tuple(val)
+       (1, 2, 3)
+       (4, 5, 6)
+       >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
+       ...     print list(val)
+       [1, 2, 3]
+       [4, 5, 6]
+       >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
+       ...     print tuple(val)
+       (1, 2, 3)
+       (4, 5, 6)
+       (7, None, None)
+       >>> for val in itergroup("123456", 3):
+       ...     print tuple(val)
+       ('1', '2', '3')
+       ('4', '5', '6')
+       >>> for val in itergroup("123456", 3):
+       ...     print repr("".join(val))
+       '123'
+       '456'
+       """
+       paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
+       nIterators = (paddedIterator, ) * count
+       return itertools.izip(*nIterators)
+
+
+def xzip(*iterators):
+       """Iterative version of builtin 'zip'."""
+       iterators = itertools.imap(iter, iterators)
+       while 1:
+               yield tuple([x.next() for x in iterators])
+
+
+def xmap(func, *iterators):
+       """Iterative version of builtin 'map'."""
+       iterators = itertools.imap(iter, iterators)
+       values_left = [1]
+
+       def values():
+               # Emulate map behaviour, i.e. shorter
+               # sequences are padded with None when
+               # they run out of values.
+               values_left[0] = 0
+               for i in range(len(iterators)):
+                       iterator = iterators[i]
+                       if iterator is None:
+                               yield None
+                       else:
+                               try:
+                                       yield iterator.next()
+                                       values_left[0] = 1
+                               except StopIteration:
+                                       iterators[i] = None
+                                       yield None
+       while 1:
+               args = tuple(values())
+               if not values_left[0]:
+                       raise StopIteration
+               yield func(*args)
+
+
+def xfilter(func, iterator):
+       """Iterative version of builtin 'filter'."""
+       iterator = iter(iterator)
+       while 1:
+               next = iterator.next()
+               if func(next):
+                       yield next
+
+
+def xreduce(func, iterator, default=None):
+       """Iterative version of builtin 'reduce'."""
+       iterator = iter(iterator)
+       try:
+               prev = iterator.next()
+       except StopIteration:
+               return default
+       single = 1
+       for next in iterator:
+               single = 0
+               prev = func(prev, next)
+       if single:
+               return func(prev, default)
+       return prev
+
+
+def daterange(begin, end, delta = datetime.timedelta(1)):
+       """
+       Form a range of dates and iterate over them.
+
+       Arguments:
+       begin -- a date (or datetime) object; the beginning of the range.
+       end   -- a date (or datetime) object; the end of the range.
+       delta -- (optional) a datetime.timedelta object; how much to step each iteration.
+                       Default step is 1 day.
+
+       Usage:
+       """
+       if not isinstance(delta, datetime.timedelta):
+               delta = datetime.timedelta(delta)
+
+       ZERO = datetime.timedelta(0)
+
+       if begin < end:
+               if delta <= ZERO:
+                       raise StopIteration
+               test = end.__gt__
+       else:
+               if delta >= ZERO:
+                       raise StopIteration
+               test = end.__lt__
+
+       while test(begin):
+               yield begin
+               begin += delta
+
+
+class LazyList(object):
+       """
+       A Sequence whose values are computed lazily by an iterator.
+
+       Module for the creation and use of iterator-based lazy lists.
+       this module defines a class LazyList which can be used to represent sequences
+       of values generated lazily. One can also create recursively defined lazy lists
+       that generate their values based on ones previously generated.
+
+       Backport to python 2.5 by Michael Pust
+       """
+
+       __author__ = 'Dan Spitz'
+
+       def __init__(self, iterable):
+               self._exhausted = False
+               self._iterator = iter(iterable)
+               self._data = []
+
+       def __len__(self):
+               """Get the length of a LazyList's computed data."""
+               return len(self._data)
+
+       def __getitem__(self, i):
+               """Get an item from a LazyList.
+               i should be a positive integer or a slice object."""
+               if isinstance(i, int):
+                       #index has not yet been yielded by iterator (or iterator exhausted
+                       #before reaching that index)
+                       if i >= len(self):
+                               self.exhaust(i)
+                       elif i < 0:
+                               raise ValueError('cannot index LazyList with negative number')
+                       return self._data[i]
+
+               #LazyList slices are iterators over a portion of the list.
+               elif isinstance(i, slice):
+                       start, stop, step = i.start, i.stop, i.step
+                       if any(x is not None and x < 0 for x in (start, stop, step)):
+                               raise ValueError('cannot index or step through a LazyList with'
+                                                               'a negative number')
+                       #set start and step to their integer defaults if they are None.
+                       if start is None:
+                               start = 0
+                       if step is None:
+                               step = 1
+
+                       def LazyListIterator():
+                               count = start
+                               predicate = (
+                                       (lambda: True)
+                                       if stop is None
+                                       else (lambda: count < stop)
+                               )
+                               while predicate():
+                                       try:
+                                               yield self[count]
+                                       #slices can go out of actual index range without raising an
+                                       #error
+                                       except IndexError:
+                                               break
+                                       count += step
+                       return LazyListIterator()
+
+               raise TypeError('i must be an integer or slice')
+
+       def __iter__(self):
+               """return an iterator over each value in the sequence,
+               whether it has been computed yet or not."""
+               return self[:]
+
+       def computed(self):
+               """Return an iterator over the values in a LazyList that have
+               already been computed."""
+               return self[:len(self)]
+
+       def exhaust(self, index = None):
+               """Exhaust the iterator generating this LazyList's values.
+               if index is None, this will exhaust the iterator completely.
+               Otherwise, it will iterate over the iterator until either the list
+               has a value for index or the iterator is exhausted.
+               """
+               if self._exhausted:
+                       return
+               if index is None:
+                       ind_range = itertools.count(len(self))
+               else:
+                       ind_range = range(len(self), index + 1)
+
+               for ind in ind_range:
+                       try:
+                               self._data.append(self._iterator.next())
+                       except StopIteration: #iterator is fully exhausted
+                               self._exhausted = True
+                               break
+
+
+class RecursiveLazyList(LazyList):
+
+       def __init__(self, prod, *args, **kwds):
+               super(RecursiveLazyList, self).__init__(prod(self, *args, **kwds))
+
+
+class RecursiveLazyListFactory:
+
+       def __init__(self, producer):
+               self._gen = producer
+
+       def __call__(self, *a, **kw):
+               return RecursiveLazyList(self._gen, *a, **kw)
+
+
+def lazylist(gen):
+       """
+       Decorator for creating a RecursiveLazyList subclass.
+       This should decorate a generator function taking the LazyList object as its
+       first argument which yields the contents of the list in order.
+
+       >>> #fibonnacci sequence in a lazy list.
+       >>> @lazylist
+       ... def fibgen(lst):
+       ...     yield 0
+       ...     yield 1
+       ...     for a, b in itertools.izip(lst, lst[1:]):
+       ...             yield a + b
+       ...
+       >>> #now fibs can be indexed or iterated over as if it were an infinitely long list containing the fibonnaci sequence
+       >>> fibs = fibgen()
+       >>>
+       >>> #prime numbers in a lazy list.
+       >>> @lazylist
+       ... def primegen(lst):
+       ...     yield 2
+       ...     for candidate in itertools.count(3): #start at next number after 2
+       ...             #if candidate is not divisible by any smaller prime numbers,
+       ...             #it is a prime.
+       ...             if all(candidate % p for p in lst.computed()):
+       ...                     yield candidate
+       ...
+       >>> #same for primes- treat it like an infinitely long list containing all prime numbers.
+       >>> primes = primegen()
+       >>> print fibs[0], fibs[1], fibs[2], primes[0], primes[1], primes[2]
+       0 1 1 2 3 5
+       >>> print list(fibs[:10]), list(primes[:10])
+       [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
+       """
+       return RecursiveLazyListFactory(gen)
+
+
+def map_func(f):
+       """
+       >>> import misc
+       >>> misc.validate_decorator(map_func)
+       """
+
+       @functools.wraps(f)
+       def wrapper(*args):
+               result = itertools.imap(f, args)
+               return result
+       return wrapper
+
+
+def reduce_func(function):
+       """
+       >>> import misc
+       >>> misc.validate_decorator(reduce_func(lambda x: x))
+       """
+
+       def decorator(f):
+
+               @functools.wraps(f)
+               def wrapper(*args):
+                       result = reduce(function, f(args))
+                       return result
+               return wrapper
+       return decorator
+
+
+def any_(iterable):
+       """
+       @note Python Version <2.5
+
+       >>> any_([True, True])
+       True
+       >>> any_([True, False])
+       True
+       >>> any_([False, False])
+       False
+       """
+
+       for element in iterable:
+               if element:
+                       return True
+       return False
+
+
+def all_(iterable):
+       """
+       @note Python Version <2.5
+
+       >>> all_([True, True])
+       True
+       >>> all_([True, False])
+       False
+       >>> all_([False, False])
+       False
+       """
+
+       for element in iterable:
+               if not element:
+                       return False
+       return True
+
+
+def for_every(pred, seq):
+       """
+       for_every takes a one argument predicate function and a sequence.
+       @param pred The predicate function should return true or false.
+       @returns true if every element in seq returns true for predicate, else returns false.
+
+       >>> for_every (lambda c: c > 5,(6,7,8,9))
+       True
+
+       @author Source:http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52907
+       """
+
+       for i in seq:
+               if not pred(i):
+                       return False
+       return True
+
+
+def there_exists(pred, seq):
+       """
+       there_exists takes a one argument predicate     function and a sequence.
+       @param pred The predicate function should return true or false.
+       @returns true if any element in seq returns true for predicate, else returns false.
+
+       >>> there_exists (lambda c: c > 5,(6,7,8,9))
+       True
+
+       @author Source:http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52907
+       """
+
+       for i in seq:
+               if pred(i):
+                       return True
+       return False
+
+
+def func_repeat(quantity, func, *args, **kwd):
+       """
+       Meant to be in connection with "reduce"
+       """
+       for i in xrange(quantity):
+               yield func(*args, **kwd)
+
+
+def function_map(preds, item):
+       """
+       Meant to be in connection with "reduce"
+       """
+       results = (pred(item) for pred in preds)
+
+       return results
+
+
+def functional_if(combiner, preds, item):
+       """
+       Combines the result of a list of predicates applied to item according to combiner
+
+       @see any, every for example combiners
+       """
+       pass_bool = lambda b: b
+
+       bool_results = function_map(preds, item)
+       return combiner(pass_bool, bool_results)
diff --git a/src/util/concurrent.py b/src/util/concurrent.py
new file mode 100644 (file)
index 0000000..503a1b4
--- /dev/null
@@ -0,0 +1,77 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+
+import os
+import errno
+import time
+import functools
+import contextlib
+
+
+def synchronized(lock):
+       """
+       Synchronization decorator.
+
+       >>> import misc
+       >>> misc.validate_decorator(synchronized(object()))
+       """
+
+       def wrap(f):
+
+               @functools.wraps(f)
+               def newFunction(*args, **kw):
+                       lock.acquire()
+                       try:
+                               return f(*args, **kw)
+                       finally:
+                               lock.release()
+               return newFunction
+       return wrap
+
+
+@contextlib.contextmanager
+def qlock(queue, gblock = True, gtimeout = None, pblock = True, ptimeout = None):
+       """
+       Locking with a queue, good for when you want to lock an item passed around
+
+       >>> import Queue
+       >>> item = 5
+       >>> lock = Queue.Queue()
+       >>> lock.put(item)
+       >>> with qlock(lock) as i:
+       ...     print i
+       5
+       """
+       item = queue.get(gblock, gtimeout)
+       try:
+               yield item
+       finally:
+               queue.put(item, pblock, ptimeout)
+
+
+@contextlib.contextmanager
+def flock(path, timeout=-1):
+       WAIT_FOREVER = -1
+       DELAY = 0.1
+       timeSpent = 0
+
+       acquired = False
+
+       while timeSpent <= timeout or timeout == WAIT_FOREVER:
+               try:
+                       fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR)
+                       acquired = True
+                       break
+               except OSError, e:
+                       if e.errno != errno.EEXIST:
+                               raise
+               time.sleep(DELAY)
+               timeSpent += DELAY
+
+       assert acquired, "Failed to grab file-lock %s within timeout %d" % (path, timeout)
+
+       try:
+               yield fd
+       finally:
+               os.unlink(path)
diff --git a/src/util/coroutines.py b/src/util/coroutines.py
new file mode 100755 (executable)
index 0000000..6d802e8
--- /dev/null
@@ -0,0 +1,610 @@
+#!/usr/bin/env python\r
+\r
+"""\r
+Uses for generators\r
+* Pull pipelining (iterators)\r
+* Push pipelining (coroutines)\r
+* State machines (coroutines)\r
+* "Cooperative multitasking" (coroutines)\r
+* Algorithm -> Object transform for cohesiveness (for example context managers) (coroutines)\r
+\r
+Design considerations\r
+* When should a stage pass on exceptions or have it thrown within it?\r
+* When should a stage pass on GeneratorExits?\r
+* Is there a way to either turn a push generator into a iterator or to use\r
+       comprehensions syntax for push generators (I doubt it)\r
+* When should the stage try and send data in both directions\r
+* Since pull generators (generators), push generators (coroutines), subroutines, and coroutines are all coroutines, maybe we should rename the push generators to not confuse them, like signals/slots? and then refer to two-way generators as coroutines\r
+** If so, make s* and co* implementation of functions\r
+"""\r
+\r
+import threading\r
+import Queue\r
+import pickle\r
+import functools\r
+import itertools\r
+import xml.sax\r
+import xml.parsers.expat\r
+\r
+\r
+def autostart(func):\r
+       """\r
+       >>> @autostart\r
+       ... def grep_sink(pattern):\r
+       ...     print "Looking for %s" % pattern\r
+       ...     while True:\r
+       ...             line = yield\r
+       ...             if pattern in line:\r
+       ...                     print line,\r
+       >>> g = grep_sink("python")\r
+       Looking for python\r
+       >>> g.send("Yeah but no but yeah but no")\r
+       >>> g.send("A series of tubes")\r
+       >>> g.send("python generators rock!")\r
+       python generators rock!\r
+       >>> g.close()\r
+       """\r
+\r
+       @functools.wraps(func)\r
+       def start(*args, **kwargs):\r
+               cr = func(*args, **kwargs)\r
+               cr.next()\r
+               return cr\r
+\r
+       return start\r
+\r
+\r
+@autostart\r
+def printer_sink(format = "%s"):\r
+       """\r
+       >>> pr = printer_sink("%r")\r
+       >>> pr.send("Hello")\r
+       'Hello'\r
+       >>> pr.send("5")\r
+       '5'\r
+       >>> pr.send(5)\r
+       5\r
+       >>> p = printer_sink()\r
+       >>> p.send("Hello")\r
+       Hello\r
+       >>> p.send("World")\r
+       World\r
+       >>> # p.throw(RuntimeError, "Goodbye")\r
+       >>> # p.send("Meh")\r
+       >>> # p.close()\r
+       """\r
+       while True:\r
+               item = yield\r
+               print format % (item, )\r
+\r
+\r
+@autostart\r
+def null_sink():\r
+       """\r
+       Good for uses like with cochain to pick up any slack\r
+       """\r
+       while True:\r
+               item = yield\r
+\r
+\r
+def itr_source(itr, target):\r
+       """\r
+       >>> itr_source(xrange(2), printer_sink())\r
+       0\r
+       1\r
+       """\r
+       for item in itr:\r
+               target.send(item)\r
+\r
+\r
+@autostart\r
+def cofilter(predicate, target):\r
+       """\r
+       >>> p = printer_sink()\r
+       >>> cf = cofilter(None, p)\r
+       >>> cf.send("")\r
+       >>> cf.send("Hello")\r
+       Hello\r
+       >>> cf.send([])\r
+       >>> cf.send([1, 2])\r
+       [1, 2]\r
+       >>> cf.send(False)\r
+       >>> cf.send(True)\r
+       True\r
+       >>> cf.send(0)\r
+       >>> cf.send(1)\r
+       1\r
+       >>> # cf.throw(RuntimeError, "Goodbye")\r
+       >>> # cf.send(False)\r
+       >>> # cf.send(True)\r
+       >>> # cf.close()\r
+       """\r
+       if predicate is None:\r
+               predicate = bool\r
+\r
+       while True:\r
+               try:\r
+                       item = yield\r
+                       if predicate(item):\r
+                               target.send(item)\r
+               except StandardError, e:\r
+                       target.throw(e.__class__, e.message)\r
+\r
+\r
+@autostart\r
+def comap(function, target):\r
+       """\r
+       >>> p = printer_sink()\r
+       >>> cm = comap(lambda x: x+1, p)\r
+       >>> cm.send(0)\r
+       1\r
+       >>> cm.send(1.0)\r
+       2.0\r
+       >>> cm.send(-2)\r
+       -1\r
+       >>> # cm.throw(RuntimeError, "Goodbye")\r
+       >>> # cm.send(0)\r
+       >>> # cm.send(1.0)\r
+       >>> # cm.close()\r
+       """\r
+       while True:\r
+               try:\r
+                       item = yield\r
+                       mappedItem = function(item)\r
+                       target.send(mappedItem)\r
+               except StandardError, e:\r
+                       target.throw(e.__class__, e.message)\r
+\r
+\r
+@autostart\r
+def append_sink(l):\r
+       """\r
+       >>> l = []\r
+       >>> apps = append_sink(l)\r
+       >>> apps.send(1)\r
+       >>> apps.send(2)\r
+       >>> apps.send(3)\r
+       >>> print l\r
+       [1, 2, 3]\r
+       """\r
+       while True:\r
+               item = yield\r
+               l.append(item)\r
+\r
+\r
+@autostart\r
+def last_n_sink(l, n = 1):\r
+       """\r
+       >>> l = []\r
+       >>> lns = last_n_sink(l)\r
+       >>> lns.send(1)\r
+       >>> lns.send(2)\r
+       >>> lns.send(3)\r
+       >>> print l\r
+       [3]\r
+       """\r
+       del l[:]\r
+       while True:\r
+               item = yield\r
+               extraCount = len(l) - n + 1\r
+               if 0 < extraCount:\r
+                       del l[0:extraCount]\r
+               l.append(item)\r
+\r
+\r
+@autostart\r
+def coreduce(target, function, initializer = None):\r
+       """\r
+       >>> reduceResult = []\r
+       >>> lns = last_n_sink(reduceResult)\r
+       >>> cr = coreduce(lns, lambda x, y: x + y, 0)\r
+       >>> cr.send(1)\r
+       >>> cr.send(2)\r
+       >>> cr.send(3)\r
+       >>> print reduceResult\r
+       [6]\r
+       >>> cr = coreduce(lns, lambda x, y: x + y)\r
+       >>> cr.send(1)\r
+       >>> cr.send(2)\r
+       >>> cr.send(3)\r
+       >>> print reduceResult\r
+       [6]\r
+       """\r
+       isFirst = True\r
+       cumulativeRef = initializer\r
+       while True:\r
+               item = yield\r
+               if isFirst and initializer is None:\r
+                       cumulativeRef = item\r
+               else:\r
+                       cumulativeRef = function(cumulativeRef, item)\r
+               target.send(cumulativeRef)\r
+               isFirst = False\r
+\r
+\r
+@autostart\r
+def cotee(targets):\r
+       """\r
+       Takes a sequence of coroutines and sends the received items to all of them\r
+\r
+       >>> ct = cotee((printer_sink("1 %s"), printer_sink("2 %s")))\r
+       >>> ct.send("Hello")\r
+       1 Hello\r
+       2 Hello\r
+       >>> ct.send("World")\r
+       1 World\r
+       2 World\r
+       >>> # ct.throw(RuntimeError, "Goodbye")\r
+       >>> # ct.send("Meh")\r
+       >>> # ct.close()\r
+       """\r
+       while True:\r
+               try:\r
+                       item = yield\r
+                       for target in targets:\r
+                               target.send(item)\r
+               except StandardError, e:\r
+                       for target in targets:\r
+                               target.throw(e.__class__, e.message)\r
+\r
+\r
+class CoTee(object):\r
+       """\r
+       >>> ct = CoTee()\r
+       >>> ct.register_sink(printer_sink("1 %s"))\r
+       >>> ct.register_sink(printer_sink("2 %s"))\r
+       >>> ct.stage.send("Hello")\r
+       1 Hello\r
+       2 Hello\r
+       >>> ct.stage.send("World")\r
+       1 World\r
+       2 World\r
+       >>> ct.register_sink(printer_sink("3 %s"))\r
+       >>> ct.stage.send("Foo")\r
+       1 Foo\r
+       2 Foo\r
+       3 Foo\r
+       >>> # ct.stage.throw(RuntimeError, "Goodbye")\r
+       >>> # ct.stage.send("Meh")\r
+       >>> # ct.stage.close()\r
+       """\r
+\r
+       def __init__(self):\r
+               self.stage = self._stage()\r
+               self._targets = []\r
+\r
+       def register_sink(self, sink):\r
+               self._targets.append(sink)\r
+\r
+       def unregister_sink(self, sink):\r
+               self._targets.remove(sink)\r
+\r
+       def restart(self):\r
+               self.stage = self._stage()\r
+\r
+       @autostart\r
+       def _stage(self):\r
+               while True:\r
+                       try:\r
+                               item = yield\r
+                               for target in self._targets:\r
+                                       target.send(item)\r
+                       except StandardError, e:\r
+                               for target in self._targets:\r
+                                       target.throw(e.__class__, e.message)\r
+\r
+\r
+def _flush_queue(queue):\r
+       while not queue.empty():\r
+               yield queue.get()\r
+\r
+\r
+@autostart\r
+def cocount(target, start = 0):\r
+       """\r
+       >>> cc = cocount(printer_sink("%s"))\r
+       >>> cc.send("a")\r
+       0\r
+       >>> cc.send(None)\r
+       1\r
+       >>> cc.send([])\r
+       2\r
+       >>> cc.send(0)\r
+       3\r
+       """\r
+       for i in itertools.count(start):\r
+               item = yield\r
+               target.send(i)\r
+\r
+\r
+@autostart\r
+def coenumerate(target, start = 0):\r
+       """\r
+       >>> ce = coenumerate(printer_sink("%r"))\r
+       >>> ce.send("a")\r
+       (0, 'a')\r
+       >>> ce.send(None)\r
+       (1, None)\r
+       >>> ce.send([])\r
+       (2, [])\r
+       >>> ce.send(0)\r
+       (3, 0)\r
+       """\r
+       for i in itertools.count(start):\r
+               item = yield\r
+               decoratedItem = i, item\r
+               target.send(decoratedItem)\r
+\r
+\r
+@autostart\r
+def corepeat(target, elem):\r
+       """\r
+       >>> cr = corepeat(printer_sink("%s"), "Hello World")\r
+       >>> cr.send("a")\r
+       Hello World\r
+       >>> cr.send(None)\r
+       Hello World\r
+       >>> cr.send([])\r
+       Hello World\r
+       >>> cr.send(0)\r
+       Hello World\r
+       """\r
+       while True:\r
+               item = yield\r
+               target.send(elem)\r
+\r
+\r
+@autostart\r
+def cointercept(target, elems):\r
+       """\r
+       >>> cr = cointercept(printer_sink("%s"), [1, 2, 3, 4])\r
+       >>> cr.send("a")\r
+       1\r
+       >>> cr.send(None)\r
+       2\r
+       >>> cr.send([])\r
+       3\r
+       >>> cr.send(0)\r
+       4\r
+       >>> cr.send("Bye")\r
+       Traceback (most recent call last):\r
+         File "/usr/lib/python2.5/doctest.py", line 1228, in __run\r
+           compileflags, 1) in test.globs\r
+         File "<doctest __main__.cointercept[5]>", line 1, in <module>\r
+           cr.send("Bye")\r
+       StopIteration\r
+       """\r
+       item = yield\r
+       for elem in elems:\r
+               target.send(elem)\r
+               item = yield\r
+\r
+\r
+@autostart\r
+def codropwhile(target, pred):\r
+       """\r
+       >>> cdw = codropwhile(printer_sink("%s"), lambda x: x)\r
+       >>> cdw.send([0, 1, 2])\r
+       >>> cdw.send(1)\r
+       >>> cdw.send(True)\r
+       >>> cdw.send(False)\r
+       >>> cdw.send([0, 1, 2])\r
+       [0, 1, 2]\r
+       >>> cdw.send(1)\r
+       1\r
+       >>> cdw.send(True)\r
+       True\r
+       """\r
+       while True:\r
+               item = yield\r
+               if not pred(item):\r
+                       break\r
+\r
+       while True:\r
+               item = yield\r
+               target.send(item)\r
+\r
+\r
+@autostart\r
+def cotakewhile(target, pred):\r
+       """\r
+       >>> ctw = cotakewhile(printer_sink("%s"), lambda x: x)\r
+       >>> ctw.send([0, 1, 2])\r
+       [0, 1, 2]\r
+       >>> ctw.send(1)\r
+       1\r
+       >>> ctw.send(True)\r
+       True\r
+       >>> ctw.send(False)\r
+       >>> ctw.send([0, 1, 2])\r
+       >>> ctw.send(1)\r
+       >>> ctw.send(True)\r
+       """\r
+       while True:\r
+               item = yield\r
+               if not pred(item):\r
+                       break\r
+               target.send(item)\r
+\r
+       while True:\r
+               item = yield\r
+\r
+\r
+@autostart\r
+def coslice(target, lower, upper):\r
+       """\r
+       >>> cs = coslice(printer_sink("%r"), 3, 5)\r
+       >>> cs.send("0")\r
+       >>> cs.send("1")\r
+       >>> cs.send("2")\r
+       >>> cs.send("3")\r
+       '3'\r
+       >>> cs.send("4")\r
+       '4'\r
+       >>> cs.send("5")\r
+       >>> cs.send("6")\r
+       """\r
+       for i in xrange(lower):\r
+               item = yield\r
+       for i in xrange(upper - lower):\r
+               item = yield\r
+               target.send(item)\r
+       while True:\r
+               item = yield\r
+\r
+\r
+@autostart\r
+def cochain(targets):\r
+       """\r
+       >>> cr = cointercept(printer_sink("good %s"), [1, 2, 3, 4])\r
+       >>> cc = cochain([cr, printer_sink("end %s")])\r
+       >>> cc.send("a")\r
+       good 1\r
+       >>> cc.send(None)\r
+       good 2\r
+       >>> cc.send([])\r
+       good 3\r
+       >>> cc.send(0)\r
+       good 4\r
+       >>> cc.send("Bye")\r
+       end Bye\r
+       """\r
+       behind = []\r
+       for target in targets:\r
+               try:\r
+                       while behind:\r
+                               item = behind.pop()\r
+                               target.send(item)\r
+                       while True:\r
+                               item = yield\r
+                               target.send(item)\r
+               except StopIteration:\r
+                       behind.append(item)\r
+\r
+\r
+@autostart\r
+def queue_sink(queue):\r
+       """\r
+       >>> q = Queue.Queue()\r
+       >>> qs = queue_sink(q)\r
+       >>> qs.send("Hello")\r
+       >>> qs.send("World")\r
+       >>> qs.throw(RuntimeError, "Goodbye")\r
+       >>> qs.send("Meh")\r
+       >>> qs.close()\r
+       >>> print [i for i in _flush_queue(q)]\r
+       [(None, 'Hello'), (None, 'World'), (<type 'exceptions.RuntimeError'>, 'Goodbye'), (None, 'Meh'), (<type 'exceptions.GeneratorExit'>, None)]\r
+       """\r
+       while True:\r
+               try:\r
+                       item = yield\r
+                       queue.put((None, item))\r
+               except StandardError, e:\r
+                       queue.put((e.__class__, e.message))\r
+               except GeneratorExit:\r
+                       queue.put((GeneratorExit, None))\r
+                       raise\r
+\r
+\r
+def decode_item(item, target):\r
+       if item[0] is None:\r
+               target.send(item[1])\r
+               return False\r
+       elif item[0] is GeneratorExit:\r
+               target.close()\r
+               return True\r
+       else:\r
+               target.throw(item[0], item[1])\r
+               return False\r
+\r
+\r
+def queue_source(queue, target):\r
+       """\r
+       >>> q = Queue.Queue()\r
+       >>> for i in [\r
+       ...     (None, 'Hello'),\r
+       ...     (None, 'World'),\r
+       ...     (GeneratorExit, None),\r
+       ...     ]:\r
+       ...     q.put(i)\r
+       >>> qs = queue_source(q, printer_sink())\r
+       Hello\r
+       World\r
+       """\r
+       isDone = False\r
+       while not isDone:\r
+               item = queue.get()\r
+               isDone = decode_item(item, target)\r
+\r
+\r
+def threaded_stage(target, thread_factory = threading.Thread):\r
+       messages = Queue.Queue()\r
+\r
+       run_source = functools.partial(queue_source, messages, target)\r
+       thread_factory(target=run_source).start()\r
+\r
+       # Sink running in current thread\r
+       return functools.partial(queue_sink, messages)\r
+\r
+\r
+@autostart\r
+def pickle_sink(f):\r
+       while True:\r
+               try:\r
+                       item = yield\r
+                       pickle.dump((None, item), f)\r
+               except StandardError, e:\r
+                       pickle.dump((e.__class__, e.message), f)\r
+               except GeneratorExit:\r
+                       pickle.dump((GeneratorExit, ), f)\r
+                       raise\r
+               except StopIteration:\r
+                       f.close()\r
+                       return\r
+\r
+\r
+def pickle_source(f, target):\r
+       try:\r
+               isDone = False\r
+               while not isDone:\r
+                       item = pickle.load(f)\r
+                       isDone = decode_item(item, target)\r
+       except EOFError:\r
+               target.close()\r
+\r
+\r
+class EventHandler(object, xml.sax.ContentHandler):\r
+\r
+       START = "start"\r
+       TEXT = "text"\r
+       END = "end"\r
+\r
+       def __init__(self, target):\r
+               object.__init__(self)\r
+               xml.sax.ContentHandler.__init__(self)\r
+               self._target = target\r
+\r
+       def startElement(self, name, attrs):\r
+               self._target.send((self.START, (name, attrs._attrs)))\r
+\r
+       def characters(self, text):\r
+               self._target.send((self.TEXT, text))\r
+\r
+       def endElement(self, name):\r
+               self._target.send((self.END, name))\r
+\r
+\r
+def expat_parse(f, target):\r
+       parser = xml.parsers.expat.ParserCreate()\r
+       parser.buffer_size = 65536\r
+       parser.buffer_text = True\r
+       parser.returns_unicode = False\r
+       parser.StartElementHandler = lambda name, attrs: target.send(('start', (name, attrs)))\r
+       parser.EndElementHandler = lambda name: target.send(('end', name))\r
+       parser.CharacterDataHandler = lambda data: target.send(('text', data))\r
+       parser.ParseFile(f)\r
+\r
+\r
+if __name__ == "__main__":\r
+       import doctest\r
+       doctest.testmod()\r
diff --git a/src/util/go_utils.py b/src/util/go_utils.py
new file mode 100644 (file)
index 0000000..66ea959
--- /dev/null
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+
+import time
+import functools
+
+import gobject
+
+
+def async(func):
+       """
+       Make a function mainloop friendly. the function will be called at the
+       next mainloop idle state.
+
+       >>> import misc
+       >>> misc.validate_decorator(async)
+       """
+
+       @functools.wraps(func)
+       def new_function(*args, **kwargs):
+
+               def async_function():
+                       func(*args, **kwargs)
+                       return False
+
+               gobject.idle_add(async_function)
+
+       return new_function
+
+
+def throttled(minDelay, queue):
+       """
+       Throttle the calls to a function by queueing all the calls that happen
+       before the minimum delay
+
+       >>> import misc
+       >>> import Queue
+       >>> misc.validate_decorator(throttled(0, Queue.Queue()))
+       """
+
+       def actual_decorator(func):
+
+               lastCallTime = [None]
+
+               def process_queue():
+                       if 0 < len(queue):
+                               func, args, kwargs = queue.pop(0)
+                               lastCallTime[0] = time.time() * 1000
+                               func(*args, **kwargs)
+                       return False
+
+               @functools.wraps(func)
+               def new_function(*args, **kwargs):
+                       now = time.time() * 1000
+                       if (
+                               lastCallTime[0] is None or
+                               (now - lastCallTime >= minDelay)
+                       ):
+                               lastCallTime[0] = now
+                               func(*args, **kwargs)
+                       else:
+                               queue.append((func, args, kwargs))
+                               lastCallDelta = now - lastCallTime[0]
+                               processQueueTimeout = int(minDelay * len(queue) - lastCallDelta)
+                               gobject.timeout_add(processQueueTimeout, process_queue)
+
+               return new_function
+
+       return actual_decorator
diff --git a/src/util/io.py b/src/util/io.py
new file mode 100644 (file)
index 0000000..aece2dd
--- /dev/null
@@ -0,0 +1,129 @@
+#!/usr/bin/env python
+
+
+from __future__ import with_statement
+
+import os
+import pickle
+import contextlib
+import itertools
+import functools
+
+
+@contextlib.contextmanager
+def change_directory(directory):
+       previousDirectory = os.getcwd()
+       os.chdir(directory)
+       currentDirectory = os.getcwd()
+
+       try:
+               yield previousDirectory, currentDirectory
+       finally:
+               os.chdir(previousDirectory)
+
+
+@contextlib.contextmanager
+def pickled(filename):
+       """
+       Here is an example usage:
+       with pickled("foo.db") as p:
+               p("users", list).append(["srid", "passwd", 23])
+       """
+
+       if os.path.isfile(filename):
+               data = pickle.load(open(filename))
+       else:
+               data = {}
+
+       def getter(item, factory):
+               if item in data:
+                       return data[item]
+               else:
+                       data[item] = factory()
+                       return data[item]
+
+       yield getter
+
+       pickle.dump(data, open(filename, "w"))
+
+
+@contextlib.contextmanager
+def redirect(object_, attr, value):
+       """
+       >>> import sys
+       ... with redirect(sys, 'stdout', open('stdout', 'w')):
+       ...     print "hello"
+       ...
+       >>> print "we're back"
+       we're back
+       """
+       orig = getattr(object_, attr)
+       setattr(object_, attr, value)
+       try:
+               yield
+       finally:
+               setattr(object_, attr, orig)
+
+
+def pathsplit(path):
+       """
+       >>> pathsplit("/a/b/c")
+       ['', 'a', 'b', 'c']
+       >>> pathsplit("./plugins/builtins.ini")
+       ['.', 'plugins', 'builtins.ini']
+       """
+       pathParts = path.split(os.path.sep)
+       return pathParts
+
+
+def commonpath(l1, l2, common=None):
+       """
+       >>> commonpath(pathsplit('/a/b/c/d'), pathsplit('/a/b/c1/d1'))
+       (['', 'a', 'b'], ['c', 'd'], ['c1', 'd1'])
+       >>> commonpath(pathsplit("./plugins/"), pathsplit("./plugins/builtins.ini"))
+       (['.', 'plugins'], [''], ['builtins.ini'])
+       >>> commonpath(pathsplit("./plugins/builtins"), pathsplit("./plugins"))
+       (['.', 'plugins'], ['builtins'], [])
+       """
+       if common is None:
+               common = []
+
+       if l1 == l2:
+               return l1, [], []
+
+       for i, (leftDir, rightDir) in enumerate(zip(l1, l2)):
+               if leftDir != rightDir:
+                       return l1[0:i], l1[i:], l2[i:]
+       else:
+               if leftDir == rightDir:
+                       i += 1
+               return l1[0:i], l1[i:], l2[i:]
+
+
+def relpath(p1, p2):
+       """
+       >>> relpath('/', '/')
+       './'
+       >>> relpath('/a/b/c/d', '/')
+       '../../../../'
+       >>> relpath('/a/b/c/d', '/a/b/c1/d1')
+       '../../c1/d1'
+       >>> relpath('/a/b/c/d', '/a/b/c1/d1/')
+       '../../c1/d1'
+       >>> relpath("./plugins/builtins", "./plugins")
+       '../'
+       >>> relpath("./plugins/", "./plugins/builtins.ini")
+       'builtins.ini'
+       """
+       sourcePath = os.path.normpath(p1)
+       destPath = os.path.normpath(p2)
+
+       (common, sourceOnly, destOnly) = commonpath(pathsplit(sourcePath), pathsplit(destPath))
+       if len(sourceOnly) or len(destOnly):
+               relParts = itertools.chain(
+                       (('..' + os.sep) * len(sourceOnly), ),
+                       destOnly,
+               )
+               return os.path.join(*relParts)
+       else:
+               return "."+os.sep
diff --git a/src/util/linux.py b/src/util/linux.py
new file mode 100644 (file)
index 0000000..43cbe2d
--- /dev/null
@@ -0,0 +1,13 @@
+#!/usr/bin/env python
+
+
+import logging
+
+
+def set_process_name(name):
+       try: # change process name for killall
+          import ctypes
+          libc = ctypes.CDLL('libc.so.6')
+          libc.prctl(15, name, 0, 0, 0)
+       except Exception, e:
+          logging.warning('Unable to set processName: %s" % e')
diff --git a/src/util/misc.py b/src/util/misc.py
new file mode 100644 (file)
index 0000000..7abecf3
--- /dev/null
@@ -0,0 +1,626 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+
+import sys
+import cPickle
+
+import functools
+import itertools
+import contextlib
+import inspect
+
+import optparse
+import traceback
+import warnings
+import string
+
+
+def printfmt(template):
+       """
+       This hides having to create the Template object and call substitute/safe_substitute on it. For example:
+
+       >>> num = 10
+       >>> word = "spam"
+       >>> printfmt("I would like to order $num units of $word, please") #doctest: +SKIP
+       I would like to order 10 units of spam, please
+       """
+       frame = inspect.stack()[-1][0]
+       try:
+               print string.Template(template).safe_substitute(frame.f_locals)
+       finally:
+               del frame
+
+
+def is_special(name):
+       return name.startswith("__") and name.endswith("__")
+
+
+def is_private(name):
+       return name.startswith("_") and not is_special(name)
+
+
+def privatize(clsName, attributeName):
+       """
+       At runtime, make an attributeName private
+
+       Example:
+       >>> class Test(object):
+       ...     pass
+       ...
+       >>> try:
+       ...     dir(Test).index("_Test__me")
+       ...     print dir(Test)
+       ... except:
+       ...     print "Not Found"
+       Not Found
+       >>> setattr(Test, privatize(Test.__name__, "me"), "Hello World")
+       >>> try:
+       ...     dir(Test).index("_Test__me")
+       ...     print "Found"
+       ... except:
+       ...     print dir(Test)
+       0
+       Found
+       >>> print getattr(Test, obfuscate(Test.__name__, "__me"))
+       Hello World
+       >>>
+       >>> is_private(privatize(Test.__name__, "me"))
+       True
+       >>> is_special(privatize(Test.__name__, "me"))
+       False
+       """
+       return "".join(["_", clsName, "__", attributeName])
+
+
+def obfuscate(clsName, attributeName):
+       """
+       At runtime, turn a private name into the obfuscated form
+
+       Example:
+       >>> class Test(object):
+       ...     __me = "Hello World"
+       ...
+       >>> try:
+       ...     dir(Test).index("_Test__me")
+       ...     print "Found"
+       ... except:
+       ...     print dir(Test)
+       0
+       Found
+       >>> print getattr(Test, obfuscate(Test.__name__, "__me"))
+       Hello World
+       >>> is_private(obfuscate(Test.__name__, "__me"))
+       True
+       >>> is_special(obfuscate(Test.__name__, "__me"))
+       False
+       """
+       return "".join(["_", clsName, attributeName])
+
+
+class PAOptionParser(optparse.OptionParser, object):
+       """
+       >>> if __name__ == '__main__':
+       ...     #parser = PAOptionParser("My usage str")
+       ...     parser = PAOptionParser()
+       ...     parser.add_posarg("Foo", help="Foo usage")
+       ...     parser.add_posarg("Bar", dest="bar_dest")
+       ...     parser.add_posarg("Language", dest='tr_type', type="choice", choices=("Python", "Other"))
+       ...     parser.add_option('--stocksym', dest='symbol')
+       ...     values, args = parser.parse_args()
+       ...     print values, args
+       ...
+
+       python mycp.py  -h
+       python mycp.py
+       python mycp.py  foo
+       python mycp.py  foo bar
+
+       python mycp.py foo bar lava
+       Usage: pa.py <Foo> <Bar> <Language> [options]
+
+       Positional Arguments:
+       Foo: Foo usage
+       Bar:
+       Language:
+
+       pa.py: error: option --Language: invalid choice: 'lava' (choose from 'Python', 'Other'
+       """
+
+       def __init__(self, *args, **kw):
+               self.posargs = []
+               super(PAOptionParser, self).__init__(*args, **kw)
+
+       def add_posarg(self, *args, **kw):
+               pa_help = kw.get("help", "")
+               kw["help"] = optparse.SUPPRESS_HELP
+               o = self.add_option("--%s" % args[0], *args[1:], **kw)
+               self.posargs.append((args[0], pa_help))
+
+       def get_usage(self, *args, **kwargs):
+               params = (' '.join(["<%s>" % arg[0] for arg in self.posargs]), '\n '.join(["%s: %s" % (arg) for arg in self.posargs]))
+               self.usage = "%%prog %s [options]\n\nPositional Arguments:\n %s" % params
+               return super(PAOptionParser, self).get_usage(*args, **kwargs)
+
+       def parse_args(self, *args, **kwargs):
+               args = sys.argv[1:]
+               args0 = []
+               for p, v in zip(self.posargs, args):
+                       args0.append("--%s" % p[0])
+                       args0.append(v)
+               args = args0 + args
+               options, args = super(PAOptionParser, self).parse_args(args, **kwargs)
+               if len(args) < len(self.posargs):
+                       msg = 'Missing value(s) for "%s"\n' % ", ".join([arg[0] for arg in self.posargs][len(args):])
+                       self.error(msg)
+               return options, args
+
+
+def explicitly(name, stackadd=0):
+       """
+       This is an alias for adding to '__all__'.  Less error-prone than using
+       __all__ itself, since setting __all__ directly is prone to stomping on
+       things implicitly exported via L{alias}.
+
+       @note Taken from PyExport (which could turn out pretty cool):
+       @li @a http://codebrowse.launchpad.net/~glyph/
+       @li @a http://glyf.livejournal.com/74356.html
+       """
+       packageVars = sys._getframe(1+stackadd).f_locals
+       globalAll = packageVars.setdefault('__all__', [])
+       globalAll.append(name)
+
+
+def public(thunk):
+       """
+       This is a decorator, for convenience.  Rather than typing the name of your
+       function twice, you can decorate a function with this.
+
+       To be real, @public would need to work on methods as well, which gets into
+       supporting types...
+
+       @note Taken from PyExport (which could turn out pretty cool):
+       @li @a http://codebrowse.launchpad.net/~glyph/
+       @li @a http://glyf.livejournal.com/74356.html
+       """
+       explicitly(thunk.__name__, 1)
+       return thunk
+
+
+def _append_docstring(obj, message):
+       if obj.__doc__ is None:
+               obj.__doc__ = message
+       else:
+               obj.__doc__ += message
+
+
+def validate_decorator(decorator):
+
+       def simple(x):
+               return x
+
+       f = simple
+       f.__name__ = "name"
+       f.__doc__ = "doc"
+       f.__dict__["member"] = True
+
+       g = decorator(f)
+
+       if f.__name__ != g.__name__:
+               print f.__name__, "!=", g.__name__
+
+       if g.__doc__ is None:
+               print decorator.__name__, "has no doc string"
+       elif not g.__doc__.startswith(f.__doc__):
+               print g.__doc__, "didn't start with", f.__doc__
+
+       if not ("member" in g.__dict__ and g.__dict__["member"]):
+               print "'member' not in ", g.__dict__
+
+
+def deprecated_api(func):
+       """
+       This is a decorator which can be used to mark functions
+       as deprecated. It will result in a warning being emitted
+       when the function is used.
+
+       >>> validate_decorator(deprecated_api)
+       """
+
+       @functools.wraps(func)
+       def newFunc(*args, **kwargs):
+               warnings.warn("Call to deprecated function %s." % func.__name__, category=DeprecationWarning)
+               return func(*args, **kwargs)
+       _append_docstring(newFunc, "\n@deprecated")
+       return newFunc
+
+
+def unstable_api(func):
+       """
+       This is a decorator which can be used to mark functions
+       as deprecated. It will result in a warning being emitted
+       when the function is used.
+
+       >>> validate_decorator(unstable_api)
+       """
+
+       @functools.wraps(func)
+       def newFunc(*args, **kwargs):
+               warnings.warn("Call to unstable API function %s." % func.__name__, category=FutureWarning)
+               return func(*args, **kwargs)
+       _append_docstring(newFunc, "\n@unstable")
+       return newFunc
+
+
+def enabled(func):
+       """
+       This decorator doesn't add any behavior
+
+       >>> validate_decorator(enabled)
+       """
+       return func
+
+
+def disabled(func):
+       """
+       This decorator disables the provided function, and does nothing
+
+       >>> validate_decorator(disabled)
+       """
+
+       @functools.wraps(func)
+       def emptyFunc(*args, **kargs):
+               pass
+       _append_docstring(emptyFunc, "\n@note Temporarily Disabled")
+       return emptyFunc
+
+
+def metadata(document=True, **kwds):
+       """
+       >>> validate_decorator(metadata(author="Ed"))
+       """
+
+       def decorate(func):
+               for k, v in kwds.iteritems():
+                       setattr(func, k, v)
+                       if document:
+                               _append_docstring(func, "\n@"+k+" "+v)
+               return func
+       return decorate
+
+
+def prop(func):
+       """Function decorator for defining property attributes
+
+       The decorated function is expected to return a dictionary
+       containing one or more of the following pairs:
+               fget - function for getting attribute value
+               fset - function for setting attribute value
+               fdel - function for deleting attribute
+       This can be conveniently constructed by the locals() builtin
+       function; see:
+       http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/205183
+       @author http://kbyanc.blogspot.com/2007/06/python-property-attribute-tricks.html
+
+       Example:
+       >>> #Due to transformation from function to property, does not need to be validated
+       >>> #validate_decorator(prop)
+       >>> class MyExampleClass(object):
+       ...     @prop
+       ...     def foo():
+       ...             "The foo property attribute's doc-string"
+       ...             def fget(self):
+       ...                     print "GET"
+       ...                     return self._foo
+       ...             def fset(self, value):
+       ...                     print "SET"
+       ...                     self._foo = value
+       ...             return locals()
+       ...
+       >>> me = MyExampleClass()
+       >>> me.foo = 10
+       SET
+       >>> print me.foo
+       GET
+       10
+       """
+       return property(doc=func.__doc__, **func())
+
+
+def print_handler(e):
+       """
+       @see ExpHandler
+       """
+       print "%s: %s" % (type(e).__name__, e)
+
+
+def print_ignore(e):
+       """
+       @see ExpHandler
+       """
+       print 'Ignoring %s exception: %s' % (type(e).__name__, e)
+
+
+def print_traceback(e):
+       """
+       @see ExpHandler
+       """
+       #print sys.exc_info()
+       traceback.print_exc(file=sys.stdout)
+
+
+def ExpHandler(handler = print_handler, *exceptions):
+       """
+       An exception handling idiom using decorators
+       Examples
+       Specify exceptions in order, first one is handled first
+       last one last.
+
+       >>> validate_decorator(ExpHandler())
+       >>> @ExpHandler(print_ignore, ZeroDivisionError)
+       ... @ExpHandler(None, AttributeError, ValueError)
+       ... def f1():
+       ...     1/0
+       >>> @ExpHandler(print_traceback, ZeroDivisionError)
+       ... def f2():
+       ...     1/0
+       >>> @ExpHandler()
+       ... def f3(*pargs):
+       ...     l = pargs
+       ...     return l[10]
+       >>> @ExpHandler(print_traceback, ZeroDivisionError)
+       ... def f4():
+       ...     return 1
+       >>>
+       >>>
+       >>> f1()
+       Ignoring ZeroDivisionError exception: integer division or modulo by zero
+       >>> f2() # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
+       Traceback (most recent call last):
+       ...
+       ZeroDivisionError: integer division or modulo by zero
+       >>> f3()
+       IndexError: tuple index out of range
+       >>> f4()
+       1
+       """
+
+       def wrapper(f):
+               localExceptions = exceptions
+               if not localExceptions:
+                       localExceptions = [Exception]
+               t = [(ex, handler) for ex in localExceptions]
+               t.reverse()
+
+               def newfunc(t, *args, **kwargs):
+                       ex, handler = t[0]
+                       try:
+                               if len(t) == 1:
+                                       return f(*args, **kwargs)
+                               else:
+                                       #Recurse for embedded try/excepts
+                                       dec_func = functools.partial(newfunc, t[1:])
+                                       dec_func = functools.update_wrapper(dec_func, f)
+                                       return dec_func(*args, **kwargs)
+                       except ex, e:
+                               return handler(e)
+
+               dec_func = functools.partial(newfunc, t)
+               dec_func = functools.update_wrapper(dec_func, f)
+               return dec_func
+       return wrapper
+
+
+class bindclass(object):
+       """
+       >>> validate_decorator(bindclass)
+       >>> class Foo(BoundObject):
+       ...      @bindclass
+       ...      def foo(this_class, self):
+       ...              return this_class, self
+       ...
+       >>> class Bar(Foo):
+       ...      @bindclass
+       ...      def bar(this_class, self):
+       ...              return this_class, self
+       ...
+       >>> f = Foo()
+       >>> b = Bar()
+       >>>
+       >>> f.foo() # doctest: +ELLIPSIS
+       (<class '...Foo'>, <...Foo object at ...>)
+       >>> b.foo() # doctest: +ELLIPSIS
+       (<class '...Foo'>, <...Bar object at ...>)
+       >>> b.bar() # doctest: +ELLIPSIS
+       (<class '...Bar'>, <...Bar object at ...>)
+       """
+
+       def __init__(self, f):
+               self.f = f
+               self.__name__ = f.__name__
+               self.__doc__ = f.__doc__
+               self.__dict__.update(f.__dict__)
+               self.m = None
+
+       def bind(self, cls, attr):
+
+               def bound_m(*args, **kwargs):
+                       return self.f(cls, *args, **kwargs)
+               bound_m.__name__ = attr
+               self.m = bound_m
+
+       def __get__(self, obj, objtype=None):
+               return self.m.__get__(obj, objtype)
+
+
+class ClassBindingSupport(type):
+       "@see bindclass"
+
+       def __init__(mcs, name, bases, attrs):
+               type.__init__(mcs, name, bases, attrs)
+               for attr, val in attrs.iteritems():
+                       if isinstance(val, bindclass):
+                               val.bind(mcs, attr)
+
+
+class BoundObject(object):
+       "@see bindclass"
+       __metaclass__ = ClassBindingSupport
+
+
+def bindfunction(f):
+       """
+       >>> validate_decorator(bindfunction)
+       >>> @bindfunction
+       ... def factorial(thisfunction, n):
+       ...      # Within this function the name 'thisfunction' refers to the factorial
+       ...      # function(with only one argument), even after 'factorial' is bound
+       ...      # to another object
+       ...      if n > 0:
+       ...              return n * thisfunction(n - 1)
+       ...      else:
+       ...              return 1
+       ...
+       >>> factorial(3)
+       6
+       """
+
+       @functools.wraps(f)
+       def bound_f(*args, **kwargs):
+               return f(bound_f, *args, **kwargs)
+       return bound_f
+
+
+class Memoize(object):
+       """
+       Memoize(fn) - an instance which acts like fn but memoizes its arguments
+       Will only work on functions with non-mutable arguments
+       @note Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52201
+
+       >>> validate_decorator(Memoize)
+       """
+
+       def __init__(self, fn):
+               self.fn = fn
+               self.__name__ = fn.__name__
+               self.__doc__ = fn.__doc__
+               self.__dict__.update(fn.__dict__)
+               self.memo = {}
+
+       def __call__(self, *args):
+               if args not in self.memo:
+                       self.memo[args] = self.fn(*args)
+               return self.memo[args]
+
+
+class MemoizeMutable(object):
+       """Memoize(fn) - an instance which acts like fn but memoizes its arguments
+       Will work on functions with mutable arguments(slower than Memoize)
+       @note Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52201
+
+       >>> validate_decorator(MemoizeMutable)
+       """
+
+       def __init__(self, fn):
+               self.fn = fn
+               self.__name__ = fn.__name__
+               self.__doc__ = fn.__doc__
+               self.__dict__.update(fn.__dict__)
+               self.memo = {}
+
+       def __call__(self, *args, **kw):
+               text = cPickle.dumps((args, kw))
+               if text not in self.memo:
+                       self.memo[text] = self.fn(*args, **kw)
+               return self.memo[text]
+
+
+callTraceIndentationLevel = 0
+
+
+def call_trace(f):
+       """
+       Synchronization decorator.
+
+       >>> validate_decorator(call_trace)
+       >>> @call_trace
+       ... def a(a, b, c):
+       ...     pass
+       >>> a(1, 2, c=3)
+       Entering a((1, 2), {'c': 3})
+       Exiting a((1, 2), {'c': 3})
+       """
+
+       @functools.wraps(f)
+       def verboseTrace(*args, **kw):
+               global callTraceIndentationLevel
+
+               print "%sEntering %s(%s, %s)" % ("\t"*callTraceIndentationLevel, f.__name__, args, kw)
+               callTraceIndentationLevel += 1
+               try:
+                       result = f(*args, **kw)
+               except:
+                       callTraceIndentationLevel -= 1
+                       print "%sException %s(%s, %s)" % ("\t"*callTraceIndentationLevel, f.__name__, args, kw)
+                       raise
+               callTraceIndentationLevel -= 1
+               print "%sExiting %s(%s, %s)" % ("\t"*callTraceIndentationLevel, f.__name__, args, kw)
+               return result
+
+       @functools.wraps(f)
+       def smallTrace(*args, **kw):
+               global callTraceIndentationLevel
+
+               print "%sEntering %s" % ("\t"*callTraceIndentationLevel, f.__name__)
+               callTraceIndentationLevel += 1
+               try:
+                       result = f(*args, **kw)
+               except:
+                       callTraceIndentationLevel -= 1
+                       print "%sException %s" % ("\t"*callTraceIndentationLevel, f.__name__)
+                       raise
+               callTraceIndentationLevel -= 1
+               print "%sExiting %s" % ("\t"*callTraceIndentationLevel, f.__name__)
+               return result
+
+       #return smallTrace
+       return verboseTrace
+
+
+@contextlib.contextmanager
+def lexical_scope(*args):
+       """
+       @note Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/520586
+       Example:
+       >>> b = 0
+       >>> with lexical_scope(1) as (a):
+       ...     print a
+       ...
+       1
+       >>> with lexical_scope(1,2,3) as (a,b,c):
+       ...     print a,b,c
+       ...
+       1 2 3
+       >>> with lexical_scope():
+       ...     d = 10
+       ...     def foo():
+       ...             pass
+       ...
+       >>> print b
+       2
+       """
+
+       frame = inspect.currentframe().f_back.f_back
+       saved = frame.f_locals.keys()
+       try:
+               if not args:
+                       yield
+               elif len(args) == 1:
+                       yield args[0]
+               else:
+                       yield args
+       finally:
+               f_locals = frame.f_locals
+               for key in (x for x in f_locals.keys() if x not in saved):
+                       del f_locals[key]
+               del frame
diff --git a/src/util/overloading.py b/src/util/overloading.py
new file mode 100644 (file)
index 0000000..89cb738
--- /dev/null
@@ -0,0 +1,256 @@
+#!/usr/bin/env python
+import new
+
+# Make the environment more like Python 3.0
+__metaclass__ = type
+from itertools import izip as zip
+import textwrap
+import inspect
+
+
+__all__ = [
+       "AnyType",
+       "overloaded"
+]
+
+
+AnyType = object
+
+
+class overloaded:
+       """
+       Dynamically overloaded functions.
+
+       This is an implementation of (dynamically, or run-time) overloaded
+       functions; also known as generic functions or multi-methods.
+
+       The dispatch algorithm uses the types of all argument for dispatch,
+       similar to (compile-time) overloaded functions or methods in C++ and
+       Java.
+
+       Most of the complexity in the algorithm comes from the need to support
+       subclasses in call signatures.  For example, if an function is
+       registered for a signature (T1, T2), then a call with a signature (S1,
+       S2) is acceptable, assuming that S1 is a subclass of T1, S2 a subclass
+       of T2, and there are no other more specific matches (see below).
+
+       If there are multiple matches and one of those doesn't *dominate* all
+       others, the match is deemed ambiguous and an exception is raised.  A
+       subtlety here: if, after removing the dominated matches, there are
+       still multiple matches left, but they all map to the same function,
+       then the match is not deemed ambiguous and that function is used.
+       Read the method find_func() below for details.
+
+       @note Python 2.5 is required due to the use of predicates any() and all().
+       @note only supports positional arguments
+
+       @author http://www.artima.com/weblogs/viewpost.jsp?thread=155514
+
+       >>> import misc
+       >>> misc.validate_decorator (overloaded)
+       >>>
+       >>>
+       >>>
+       >>>
+       >>> #################
+       >>> #Basics, with reusing names and without
+       >>> @overloaded
+       ... def foo(x):
+       ...     "prints x"
+       ...     print x
+       ...
+       >>> @foo.register(int)
+       ... def foo(x):
+       ...     "prints the hex representation of x"
+       ...     print hex(x)
+       ...
+       >>> from types import DictType
+       >>> @foo.register(DictType)
+       ... def foo_dict(x):
+       ...     "prints the keys of x"
+       ...     print [k for k in x.iterkeys()]
+       ...
+       >>> #combines all of the doc strings to help keep track of the specializations
+       >>> foo.__doc__  # doctest: +ELLIPSIS
+       "prints x\\n\\n...overloading.foo (<type 'int'>):\\n\\tprints the hex representation of x\\n\\n...overloading.foo_dict (<type 'dict'>):\\n\\tprints the keys of x"
+       >>> foo ("text")
+       text
+       >>> foo (10) #calling the specialized foo
+       0xa
+       >>> foo ({3:5, 6:7}) #calling the specialization foo_dict
+       [3, 6]
+       >>> foo_dict ({3:5, 6:7}) #with using a unique name, you still have the option of calling the function directly
+       [3, 6]
+       >>>
+       >>>
+       >>>
+       >>>
+       >>> #################
+       >>> #Multiple arguments, accessing the default, and function finding
+       >>> @overloaded
+       ... def two_arg (x, y):
+       ...     print x,y
+       ...
+       >>> @two_arg.register(int, int)
+       ... def two_arg_int_int (x, y):
+       ...     print hex(x), hex(y)
+       ...
+       >>> @two_arg.register(float, int)
+       ... def two_arg_float_int (x, y):
+       ...     print x, hex(y)
+       ...
+       >>> @two_arg.register(int, float)
+       ... def two_arg_int_float (x, y):
+       ...     print hex(x), y
+       ...
+       >>> two_arg.__doc__ # doctest: +ELLIPSIS
+       "...overloading.two_arg_int_int (<type 'int'>, <type 'int'>):\\n\\n...overloading.two_arg_float_int (<type 'float'>, <type 'int'>):\\n\\n...overloading.two_arg_int_float (<type 'int'>, <type 'float'>):"
+       >>> two_arg(9, 10)
+       0x9 0xa
+       >>> two_arg(9.0, 10)
+       9.0 0xa
+       >>> two_arg(15, 16.0)
+       0xf 16.0
+       >>> two_arg.default_func(9, 10)
+       9 10
+       >>> two_arg.find_func ((int, float)) == two_arg_int_float
+       True
+       >>> (int, float) in two_arg
+       True
+       >>> (str, int) in two_arg
+       False
+       >>>
+       >>>
+       >>>
+       >>> #################
+       >>> #wildcard
+       >>> @two_arg.register(AnyType, str)
+       ... def two_arg_any_str (x, y):
+       ...     print x, y.lower()
+       ...
+       >>> two_arg("Hello", "World")
+       Hello world
+       >>> two_arg(500, "World")
+       500 world
+       """
+
+       def __init__(self, default_func):
+               # Decorator to declare new overloaded function.
+               self.registry = {}
+               self.cache = {}
+               self.default_func = default_func
+               self.__name__ = self.default_func.__name__
+               self.__doc__ = self.default_func.__doc__
+               self.__dict__.update (self.default_func.__dict__)
+
+       def __get__(self, obj, type=None):
+               if obj is None:
+                       return self
+               return new.instancemethod(self, obj)
+
+       def register(self, *types):
+               """
+               Decorator to register an implementation for a specific set of types.
+
+               .register(t1, t2)(f) is equivalent to .register_func((t1, t2), f).
+               """
+
+               def helper(func):
+                       self.register_func(types, func)
+
+                       originalDoc = self.__doc__ if self.__doc__ is not None else ""
+                       typeNames = ", ".join ([str(type) for type in types])
+                       typeNames = "".join ([func.__module__+".", func.__name__, " (", typeNames, "):"])
+                       overloadedDoc = ""
+                       if func.__doc__ is not None:
+                               overloadedDoc = textwrap.fill (func.__doc__, width=60, initial_indent="\t", subsequent_indent="\t")
+                       self.__doc__ = "\n".join ([originalDoc, "", typeNames, overloadedDoc]).strip()
+
+                       new_func = func
+
+                       #Masking the function, so we want to take on its traits
+                       if func.__name__ == self.__name__:
+                               self.__dict__.update (func.__dict__)
+                               new_func = self
+                       return new_func
+
+               return helper
+
+       def register_func(self, types, func):
+               """Helper to register an implementation."""
+               self.registry[tuple(types)] = func
+               self.cache = {} # Clear the cache (later we can optimize this).
+
+       def __call__(self, *args):
+               """Call the overloaded function."""
+               types = tuple(map(type, args))
+               func = self.cache.get(types)
+               if func is None:
+                       self.cache[types] = func = self.find_func(types)
+               return func(*args)
+
+       def __contains__ (self, types):
+               return self.find_func(types) is not self.default_func
+
+       def find_func(self, types):
+               """Find the appropriate overloaded function; don't call it.
+
+               @note This won't work for old-style classes or classes without __mro__
+               """
+               func = self.registry.get(types)
+               if func is not None:
+                       # Easy case -- direct hit in registry.
+                       return func
+
+               # Phillip Eby suggests to use issubclass() instead of __mro__.
+               # There are advantages and disadvantages.
+
+               # I can't help myself -- this is going to be intense functional code.
+               # Find all possible candidate signatures.
+               mros = tuple(inspect.getmro(t) for t in types)
+               n = len(mros)
+               candidates = [sig for sig in self.registry
+                               if len(sig) == n and
+                                       all(t in mro for t, mro in zip(sig, mros))]
+
+               if not candidates:
+                       # No match at all -- use the default function.
+                       return self.default_func
+               elif len(candidates) == 1:
+                       # Unique match -- that's an easy case.
+                       return self.registry[candidates[0]]
+
+               # More than one match -- weed out the subordinate ones.
+
+               def dominates(dom, sub,
+                               orders=tuple(dict((t, i) for i, t in enumerate(mro))
+                                                       for mro in mros)):
+                       # Predicate to decide whether dom strictly dominates sub.
+                       # Strict domination is defined as domination without equality.
+                       # The arguments dom and sub are type tuples of equal length.
+                       # The orders argument is a precomputed auxiliary data structure
+                       # giving dicts of ordering information corresponding to the
+                       # positions in the type tuples.
+                       # A type d dominates a type s iff order[d] <= order[s].
+                       # A type tuple (d1, d2, ...) dominates a type tuple of equal length
+                       # (s1, s2, ...) iff d1 dominates s1, d2 dominates s2, etc.
+                       if dom is sub:
+                               return False
+                       return all(order[d] <= order[s] for d, s, order in zip(dom, sub, orders))
+
+               # I suppose I could inline dominates() but it wouldn't get any clearer.
+               candidates = [cand
+                               for cand in candidates
+                                       if not any(dominates(dom, cand) for dom in candidates)]
+               if len(candidates) == 1:
+                       # There's exactly one candidate left.
+                       return self.registry[candidates[0]]
+
+               # Perhaps these multiple candidates all have the same implementation?
+               funcs = set(self.registry[cand] for cand in candidates)
+               if len(funcs) == 1:
+                       return funcs.pop()
+
+               # No, the situation is irreducibly ambiguous.
+               raise TypeError("ambigous call; types=%r; candidates=%r" %
+                                               (types, candidates))