| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565 |
- # File: objects.py
- # Library: DOPAL - DO Python Azureus Library
- #
- # This program is free software; you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation; version 2 of the License.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details ( see the COPYING file ).
- #
- # You should have received a copy of the GNU General Public License
- # along with this program; if not, write to the Free Software
- # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
- '''
- Defines the object layer framework.
- '''
- from dopal.core import ExtendedAzureusConnection
- from dopal.errors import AzMethodError, InvalidObjectIDError, \
- RemoteMethodError, StaleObjectReferenceError, ConnectionlessObjectError, \
- NonRefreshableConnectionlessObjectError, MissingRemoteAttributeError, \
- NonRefreshableIncompleteObjectError, NonRefreshableObjectError
- import dopal.utils
- class AzureusObjectConnection(ExtendedAzureusConnection):
- '''
- This connection class generates remote representations of each object available in Azureus.
- @ivar is_persistent_connection: Boolean indicating whether the connection should be persistent. Default is C{False}.
- @ivar converter: Callable object which will be used to convert response data into objects.
- This will usually be a L{RemoteObjectConverter<dopal.convert.RemoteObjectConverter>} instance which will convert the results of method invocations into objects. A value must be assigned for this object to work - no suitable default is provided automatically.
- '''
- def __init__(self): # AzureusObjectConnection
- ExtendedAzureusConnection.__init__(self)
- self.is_persistent_connection = False
- self.converter = None
- self.cached_plugin_interface_object = None
- def _on_reconnect(self): # AzureusObjectConnection
- if not self.is_persistent_connection:
- self.cached_plugin_interface_object = None
- def get_plugin_interface(self): # AzureusObjectConnection
- # XXX: Add docstring.
- obj = self.cached_plugin_interface_object
- if self.cached_plugin_interface_object is not None:
- # Try to verify that it exists.
- #
- # Why do we verify the object? Well, we just want to ensure that
- # the object we return here is valid. It would be valid if we
- # returned getPluginInterface. If the object needs repairing,
- # then it is better to do it immediately.
- #
- # Why do we not rely on the object just sorting itself out?
- # Well, the default definitions for extracting the object from the
- # root will use the plugin interface object as the root.
- #
- try:
- self.verify_objects([self.cached_plugin_interface_object])
- except NonRefreshableObjectError:
- # Subclasses of this error will occur if there's a problem
- # refreshing the object (for whatever reason). Refreshing
- # it will only occur if the object is not valid.
- #
- # If, for whatever reason, our cached plugin interface hasn't
- # repaired itself, we'll just lose the cached version and get
- # a new object.
- #
- # Why do we not just get a plugin interface object and update
- # the cached plugin interface object? If the object is
- # 'broken', or object persistency is not enabled, there's no
- # reason to repair it - we wouldn't normally do that for any
- # other object.
- #
- # But it is important we return a valid object.
- self.cached_plugin_interface_object = None
- if self.cached_plugin_interface_object is None:
- self.cached_plugin_interface_object = self.getPluginInterface()
- return self.cached_plugin_interface_object
- def getPluginInterface(self): # AzureusObjectConnection
- return self.invoke_object_method(None, 'getSingleton', (), 'PluginInterface')
- # Invoke the remote method - nothing regarding object persistency is
- # handled here.
- def _invoke_object_method(self, az_object, method_name, method_args, result_type=None): # AzureusObjectConnection
- if az_object is None:
- az_object_id = None
- else:
- az_object_id = az_object.get_object_id()
- # We don't need to extract the object ID's for objects which are in
- # method_args - they will be an instance of one of the wrapper type
- # classes, which will be appropriate enough to pull out the correct
- # type to use.
- response = self.invoke_remote_method(az_object_id, method_name, method_args)
- result = self.converter(response.response_data, result_type=result_type)
- return result
- def invoke_object_method(self, az_object, method_name, method_args, result_type=None): # AzureusObjectConnection
- objects = [obj for obj in method_args if isinstance(obj, RemoteObject)]
- if az_object is not None:
- objects.insert(0, az_object)
- self.verify_objects(objects)
- try:
- return self._invoke_object_method(az_object, method_name, method_args, result_type)
- except InvalidObjectIDError, error:
- # XXX: TODO, this exception is likely to be one of two subclasses
- # You don't need to call is_connection_valid, since you'll know
- # from the subclasses error. It's an unnecessary method call - fix
- # it!
- if not self.is_persistent_connection:
- raise
- if self.is_connection_valid():
- raise
- self.establish_connection()
- self.verify_objects(objects)
- # Two very quick failures of this type is unlikely to happen -
- # it is more likely to be a logic error in this case, so we
- # don't retry if it fails again.
- return self._invoke_object_method(az_object, method_name, method_args, result_type)
- def verify_objects(self, objects): # AzureusObjectConnection
- # I did write this as a list expression initially, but I guess we
- # should keep it readable.
- has_refreshed_objects = False
- for obj in objects:
- if obj.__connection_id__ != self.connection_id:
- if self.is_persistent_connection:
- obj._refresh_object(self)
- has_refreshed_objects = True
- else:
- raise StaleObjectReferenceError, obj
- return has_refreshed_objects
- class RemoteObject(object):
- def __init__(self, connection, object_id, attributes=None): # RemoteObject
- if connection is None:
- self.__connection_id__ = None
- elif isinstance(connection, AzureusObjectConnection):
- self.__connection_id__ = connection.connection_id
- else:
- err = "connection must be instance of AzureusObjectConnection: %s"
- raise ValueError, err % connection
- self.__connection__ = connection
- self.__object_id__ = object_id
- if attributes is not None:
- self.update_remote_data(attributes)
- def __repr__(self): # RemoteObject
- txt = "<%s object at 0x%08X" % (self.__class__.__name__, id(self))
- # "sid" stands for short ID.
- sid = self.get_short_object_id()
- if sid is not None:
- txt += ", sid=%s" % sid
- return txt + ">"
- def __str__(self): # RemoteObject
- sid = self.get_short_object_id()
- if sid is None:
- return RemoteObject.__repr__(self)
- else:
- return "<%s, sid=%s>" % (self.__class__.__name__, sid)
- def get_short_object_id(self):
- if self.__object_id__ is None:
- return None
- return dopal.utils.make_short_object_id(self.__object_id__)
- def get_object_id(self): # RemoteObject
- return self.__object_id__
- def get_remote_type(self): # RemoteObject
- if not hasattr(self, 'get_xml_type'):
- return None
- return self.get_xml_type()
- def get_remote_attributes(self): # RemoteObject
- result = {}
- result['__connection__'] = self.__connection__
- result['__connection_id__'] = self.__connection_id__
- result['__object_id__'] = self.__object_id__
- return result
- # set_remote_attribute and update_remote_data are very closely
- # linked - the former sets one attribute at a time while the
- # other sets multiple attributes together. It is recommended
- # that set_remote_attribute is not overridden, but
- # update_remote_data is instead. If you choose to override
- # set_remote_attribute, you should override update_remote_data
- # to use set_remote_attribute.
- def set_remote_attribute(self, name, value): # RemoteObject
- return self.update_remote_data({name: value})
- def update_remote_data(self, attribute_data): # RemoteObject
- for key, value in attribute_data.items():
- setattr(self, key, value)
- def get_connection(self): # RemoteObject
- if self.__connection__ is None:
- raise ConnectionlessObjectError, self
- return self.__connection__
- # Exits quietly if the current connection is valid.
- #
- # If it is invalid, then this object's _refresh_object method will be
- # called instead to retrieve a new object ID (if applicable), but only
- # if this object's connection is a persistent one. If not, it will raise
- # a StaleObjectReferenceError.
- def verify_connection(self): # RemoteObject
- return self.get_connection().verify_objects([self])
- def refresh_object(self): # RemoteObject
- '''
- Updates the remote attributes on this object.
- @raise NonRefreshableConnectionlessObjectError: If the object is not
- attached to a connection.
- @return: None
- '''
- try:
- if not self.verify_connection():
- self._refresh_object(self.__connection__)
- except ConnectionlessObjectError:
- raise NonRefreshableConnectionlessObjectError, self
- def _refresh_object(self, connection_to_use): # RemoteObject
- '''
- Internal method which refreshes the attributes on the object.
- This method actually performs two different functionalities.
- If the connection to use is the same as the one already attached,
- with the same connection ID, then a refresh will take place.
- If the connection is either a different connection, or the connection
- ID is different, then an attempt will be made to retrieve the
- equivalent object to update the attributes.
- @param connection_to_use: The connection object to update with.
- @type connection_to_use: L{AzureusObjectConnection}
- @raise NonRefreshableObjectTypeError: Raised when the object type is
- not one which can be refreshed on broken connections.
- @raise NonRefreshableIncompleteObjectError: Raised when the object is
- missing certain attributes which prevents it being refreshed on broken
- connections.
- @return: None
- '''
- # If the object is still valid, let's use the refresh method.
- if (self.__connection__ == connection_to_use) and \
- self.__connection_id__ == connection_to_use.connection_id:
- new_object = connection_to_use.invoke_object_method(
- self, '_refresh', (), result_type=self.get_xml_type())
- # The object is no longer valid. Let's grab the equivalent object.
- else:
- # Special case - if the object is the cached plugin interface
- # object, then we need to avoid calling get_plugin_interface.
- #
- # Why? Because that'll pick up that the object is invalid, and
- # then attempt to refresh it. Recursive infinite loop.
- #
- # So in that case, we just get a plugin interface object
- # directly.
- if connection_to_use.cached_plugin_interface_object is self:
- new_object = connection_to_use.getPluginInterface()
- else:
- root = connection_to_use.get_plugin_interface()
- new_object = self._get_self_from_root_object(root)
- del root
- # Get the attributes...
- new_data = new_object.get_remote_attributes()
- # (Make sure that the important attributes are there...)
- if __debug__:
- attrs = ['__connection__', '__connection_id__', '__object_id__']
- for key in attrs:
- if key not in new_data:
- err = "%r.get_remote_attributes() is missing values!"
- raise AssertionError, err % self
- del attrs, key
- # Update the values.
- self.update_remote_data(new_data)
- # This method is used to locate the remote equivalent object from the
- # plugin interface object. If the object cannot be retrieved from the
- # PluginInterface, you should raise a NonRefreshableObjectTypeError
- # instead (this is the default behaviour).
- def _get_self_from_root_object(self, plugin_interface): # RemoteObject
- raise NonRefreshableObjectError, self
- def invoke_object_method(self, method, method_args, result_type=None): # RemoteObject
- try:
- return self.get_connection().invoke_object_method(self, method, method_args, result_type=result_type)
- except RemoteMethodError, error:
- # There's three different ways an error can be generated here:
- # 1) _handle_invocation_error raises an error - this will have
- # the traceback of where it was raised.
- # 2) _handle_invocation_error returns an error object - this
- # will have the traceback of the original exception.
- # 3) _handle_invocation_error returns None - this will just
- # reraise the original error.
- error = self._handle_invocation_error(error, method, method_args)
- if error is not None:
- import sys
- raise error, None, sys.exc_info()[2]
- raise
- def _handle_invocation_error(self, error, method_name, method_args): # RemoteObject
- # Default behaviour - just reraise the old error.
- return None
- # Called by the converter classes to determine the type of a remote
- # attribute.
- def _get_type_for_attribute(self, attrib_name, mapping_key=None): # RemoteObject
- return None
- class RemoteConstantsMetaclass(type):
- def __init__(cls, name, bases, cls_dict):
- super(RemoteConstantsMetaclass, cls).__init__(name, bases, cls_dict)
- if hasattr(cls, '__az_constants__'):
- for key, value in cls.__az_constants__.items():
- setattr(cls, key, value)
- # This just used for interrogation purposes - the guts of this function will
- # be used to build other functions (see below).
- #
- # Poor little function - ends up being consumed and tossed aside, like a
- # Hollow devouring a human soul.
- def _invoke_remote_method(self, *args, **kwargs):
- from dopal.utils import handle_kwargs
- kwargs = handle_kwargs(kwargs, result_type=None)
- return self.invoke_object_method(__funcname__, args, **kwargs)
- from dopal.utils import MethodFactory
- _methodobj = MethodFactory(_invoke_remote_method)
- make_instance_remote_method = _methodobj.make_instance_method
- make_class_remote_method = _methodobj.make_class_method
- del _methodobj, MethodFactory
- from dopal.aztypes import AzureusMethods
- class RemoteMethodMetaclass(type):
- def __init__(cls, name, bases, cls_dict):
- super(RemoteMethodMetaclass, cls).__init__(name, bases, cls_dict)
- az_key = '__az_methods__'
- if az_key not in cls_dict:
- methodsobj = AzureusMethods()
- for base in bases:
- if hasattr(base, az_key):
- methodsobj.update(getattr(base, az_key))
- setattr(cls, az_key, methodsobj)
- else:
- methodsobj = getattr(cls, az_key)
- # Create the real methods based on those in __az_methods__.
- for method_name in methodsobj.get_method_names():
- if not hasattr(cls, method_name):
- _mobj = make_class_remote_method(method_name, cls)
- setattr(cls, method_name, _mobj)
- class RemoteMethodMixin(object):
- __use_dynamic_methods__ = False
- __use_type_checking__ = True
- def __getattr__(self, name):
- # Anything which starts with an underscore is unlikely to be a public
- # method.
- if (not name.startswith('_')) and self.__use_dynamic_methods__:
- return self._get_remote_method_on_demand(name)
- _superclass = super(RemoteMethodMixin, self)
- # Influenced by code here:
- # http://aspn.activestate.com/ASPN/Mail/Message/python-list/1620146
- #
- # The problem is that we can't use the super object to get a
- # __getattr__ method for the appropriate class.
- self_mro = list(self.__class__.__mro__)
- for cls in self_mro[self_mro.index(RemoteMethodMixin)+1:]:
- if hasattr(cls, '__getattr__'):
- return cls.__getattr__(self, name)
- else:
- # Isn't there something I can call to fall back on default
- # behaviour?
- text = "'%s' object has no attribute '%s'"
- raise AttributeError, text % (type(self).__name__, name)
- # Used to create a remote method object on demand.
- def _get_remote_method_on_demand(self, name):
- return make_instance_remote_method(name, self)
- def invoke_object_method(self, method, method_args, result_type=None):
- if self.__use_type_checking__:
- try:
- az_methods = self.__az_methods__
- except AttributeError:
- if not self.__use_dynamic_methods__:
- raise RuntimeError, "%s uses type checking, but has no methods to check against" % type(self).__name__
- else:
- try:
- method_args, result_type = \
- az_methods.wrap_args(method, method_args)
- except AzMethodError:
- if not self.__use_dynamic_methods__:
- raise
- return super(RemoteMethodMixin, self).invoke_object_method(method, method_args, result_type=result_type)
- class RemoteAttributeMetaclass(type):
- # XXX: What the hell is this meant to do!?
- def __init__(cls, name, bases, cls_dict):
- deft_names = '__default_remote_attribute_names__'
- az_attrs = '__az_attributes__'
- attr_dict = cls_dict.setdefault(deft_names, {})
- attr_dict.update(cls_dict.get(az_attrs, {}))
- for base in bases:
- attr_dict.update(getattr(base, deft_names, {}))
- attr_dict.update(getattr(base, az_attrs, {}))
- setattr(cls, deft_names, attr_dict)
- super(RemoteAttributeMetaclass, cls).__init__(name, bases, cls_dict)
- class RemoteAttributesMixin(object):
- __default_remote_attribute_names__ = {}
- __reset_attributes_on_refresh__ = False
- __protect_remote_attributes__ = True
- def __init__(self, *args, **kwargs):
- # Class attribute becomes instance attribute.
- super(RemoteAttributesMixin, self).__init__(*args, **kwargs)
- self.__remote_attribute_names__ = self.__default_remote_attribute_names__.copy()
- def __getattr__(self, name):
- if name in self.__remote_attribute_names__:
- raise MissingRemoteAttributeError, name
- # Influenced by code here:
- # http://aspn.activestate.com/ASPN/Mail/Message/python-list/1620146
- self_mro = list(self.__class__.__mro__)
- for cls in self_mro[self_mro.index(RemoteAttributesMixin)+1:]:
- if hasattr(cls, '__getattr__'):
- return cls.__getattr__(self, name)
- else:
- # Isn't there something I can call to fall back on default
- # behaviour?
- text = "'%s' object has no attribute '%s'"
- raise AttributeError, text % (type(self).__name__, name)
- def __setattr__(self, name, value):
- if self.__protect_remote_attributes__ and not name.startswith('__'):
- if name in self.__remote_attribute_names__:
- err = "cannot set remote attribute directly: %s"
- raise AttributeError, err % name
- return super(RemoteAttributesMixin, self).__setattr__(name, value)
- def set_remote_attribute(self, name, value):
- if name not in self.__remote_attribute_names__:
- self.__remote_attribute_names__[name] = None
- return super(RemoteAttributesMixin, self).__setattr__(name, value)
- def get_remote_attributes(self):
- result = super(RemoteAttributesMixin, self).get_remote_attributes()
- for attribute in self.__remote_attribute_names__:
- if hasattr(self, attribute):
- result[attribute] = getattr(self, attribute)
- return result
- def is_remote_attribute(self, name):
- return name in self.__remote_attribute_names__
- def update_remote_data(self, remote_attribute_dict):
- if self.__reset_attributes_on_refresh__:
- for attrib in self.__remote_attribute_names__:
- try:
- delattr(self, attrib)
- except AttributeError:
- pass
- _super = super(RemoteAttributesMixin, self)
- # XXX: Do a better fix than this!
- pra_value = self.__protect_remote_attributes__
- self.__protect_remote_attributes__ = False
- try:
- return _super.update_remote_data(remote_attribute_dict)
- finally:
- self.__protect_remote_attributes__ = pra_value
- def _get_type_for_attribute(self, name, mapping_key=None):
- if mapping_key is not None:
- key_to_use = name + ',' + mapping_key
- else:
- key_to_use = name
- result = self.__remote_attribute_names__.get(key_to_use)
- if result is not None:
- return result
- else:
- import dopal
- if dopal.__dopal_mode__ == 1:
- raise RuntimeError, (self, key_to_use)
- _superfunc = super(RemoteAttributesMixin, self)._get_type_for_attribute
- return _superfunc(name, mapping_key)
- class AzureusObjectMetaclass(RemoteConstantsMetaclass, RemoteMethodMetaclass, RemoteAttributeMetaclass):
- pass
- class AzureusObject(RemoteAttributesMixin, RemoteMethodMixin, RemoteObject):
- __metaclass__ = AzureusObjectMetaclass
- def _get_self_from_root_object(self, plugin_interface):
- # XXX: Err, this is a bit incorrect - it should be get_remote_type.
- # But it will do for now. Need to think more carefully about
- # the responsibilities of the two methods.
- if hasattr(self, 'get_xml_type'):
- from dopal.persistency import get_equivalent_object_from_root
- return get_equivalent_object_from_root(self, plugin_interface)
- return super(AzureusObject, self)._get_self_from_root_object(plugin_interface)
- class TypelessRemoteObject(RemoteAttributesMixin, RemoteMethodMixin, RemoteObject):
- __use_dynamic_methods__ = True
- TYPELESS_CLASS_MAP = {None: TypelessRemoteObject}
- # XXX: Define converter here?
- # Add type checking code (though this proably should be core)
- # Add some code to read data from statistics file (what level should this be at?)
- ## Allow some code to make link_error_handler assignable
- # Converter - needs to have some default behaviours (easily changeable):
- # a) Atoms - what to do if no type is suggested. (not so important this one)
- # b) Objects - what to do if no class is given (if no id is given?)
|