1
0

objects.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. # File: objects.py
  2. # Library: DOPAL - DO Python Azureus Library
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; version 2 of the License.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details ( see the COPYING file ).
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with this program; if not, write to the Free Software
  15. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  16. '''
  17. Defines the object layer framework.
  18. '''
  19. from dopal.core import ExtendedAzureusConnection
  20. from dopal.errors import AzMethodError, InvalidObjectIDError, \
  21. RemoteMethodError, StaleObjectReferenceError, ConnectionlessObjectError, \
  22. NonRefreshableConnectionlessObjectError, MissingRemoteAttributeError, \
  23. NonRefreshableIncompleteObjectError, NonRefreshableObjectError
  24. import dopal.utils
  25. class AzureusObjectConnection(ExtendedAzureusConnection):
  26. '''
  27. This connection class generates remote representations of each object available in Azureus.
  28. @ivar is_persistent_connection: Boolean indicating whether the connection should be persistent. Default is C{False}.
  29. @ivar converter: Callable object which will be used to convert response data into objects.
  30. 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.
  31. '''
  32. def __init__(self): # AzureusObjectConnection
  33. ExtendedAzureusConnection.__init__(self)
  34. self.is_persistent_connection = False
  35. self.converter = None
  36. self.cached_plugin_interface_object = None
  37. def _on_reconnect(self): # AzureusObjectConnection
  38. if not self.is_persistent_connection:
  39. self.cached_plugin_interface_object = None
  40. def get_plugin_interface(self): # AzureusObjectConnection
  41. # XXX: Add docstring.
  42. obj = self.cached_plugin_interface_object
  43. if self.cached_plugin_interface_object is not None:
  44. # Try to verify that it exists.
  45. #
  46. # Why do we verify the object? Well, we just want to ensure that
  47. # the object we return here is valid. It would be valid if we
  48. # returned getPluginInterface. If the object needs repairing,
  49. # then it is better to do it immediately.
  50. #
  51. # Why do we not rely on the object just sorting itself out?
  52. # Well, the default definitions for extracting the object from the
  53. # root will use the plugin interface object as the root.
  54. #
  55. try:
  56. self.verify_objects([self.cached_plugin_interface_object])
  57. except NonRefreshableObjectError:
  58. # Subclasses of this error will occur if there's a problem
  59. # refreshing the object (for whatever reason). Refreshing
  60. # it will only occur if the object is not valid.
  61. #
  62. # If, for whatever reason, our cached plugin interface hasn't
  63. # repaired itself, we'll just lose the cached version and get
  64. # a new object.
  65. #
  66. # Why do we not just get a plugin interface object and update
  67. # the cached plugin interface object? If the object is
  68. # 'broken', or object persistency is not enabled, there's no
  69. # reason to repair it - we wouldn't normally do that for any
  70. # other object.
  71. #
  72. # But it is important we return a valid object.
  73. self.cached_plugin_interface_object = None
  74. if self.cached_plugin_interface_object is None:
  75. self.cached_plugin_interface_object = self.getPluginInterface()
  76. return self.cached_plugin_interface_object
  77. def getPluginInterface(self): # AzureusObjectConnection
  78. return self.invoke_object_method(None, 'getSingleton', (), 'PluginInterface')
  79. # Invoke the remote method - nothing regarding object persistency is
  80. # handled here.
  81. def _invoke_object_method(self, az_object, method_name, method_args, result_type=None): # AzureusObjectConnection
  82. if az_object is None:
  83. az_object_id = None
  84. else:
  85. az_object_id = az_object.get_object_id()
  86. # We don't need to extract the object ID's for objects which are in
  87. # method_args - they will be an instance of one of the wrapper type
  88. # classes, which will be appropriate enough to pull out the correct
  89. # type to use.
  90. response = self.invoke_remote_method(az_object_id, method_name, method_args)
  91. result = self.converter(response.response_data, result_type=result_type)
  92. return result
  93. def invoke_object_method(self, az_object, method_name, method_args, result_type=None): # AzureusObjectConnection
  94. objects = [obj for obj in method_args if isinstance(obj, RemoteObject)]
  95. if az_object is not None:
  96. objects.insert(0, az_object)
  97. self.verify_objects(objects)
  98. try:
  99. return self._invoke_object_method(az_object, method_name, method_args, result_type)
  100. except InvalidObjectIDError, error:
  101. # XXX: TODO, this exception is likely to be one of two subclasses
  102. # You don't need to call is_connection_valid, since you'll know
  103. # from the subclasses error. It's an unnecessary method call - fix
  104. # it!
  105. if not self.is_persistent_connection:
  106. raise
  107. if self.is_connection_valid():
  108. raise
  109. self.establish_connection()
  110. self.verify_objects(objects)
  111. # Two very quick failures of this type is unlikely to happen -
  112. # it is more likely to be a logic error in this case, so we
  113. # don't retry if it fails again.
  114. return self._invoke_object_method(az_object, method_name, method_args, result_type)
  115. def verify_objects(self, objects): # AzureusObjectConnection
  116. # I did write this as a list expression initially, but I guess we
  117. # should keep it readable.
  118. has_refreshed_objects = False
  119. for obj in objects:
  120. if obj.__connection_id__ != self.connection_id:
  121. if self.is_persistent_connection:
  122. obj._refresh_object(self)
  123. has_refreshed_objects = True
  124. else:
  125. raise StaleObjectReferenceError, obj
  126. return has_refreshed_objects
  127. class RemoteObject(object):
  128. def __init__(self, connection, object_id, attributes=None): # RemoteObject
  129. if connection is None:
  130. self.__connection_id__ = None
  131. elif isinstance(connection, AzureusObjectConnection):
  132. self.__connection_id__ = connection.connection_id
  133. else:
  134. err = "connection must be instance of AzureusObjectConnection: %s"
  135. raise ValueError, err % connection
  136. self.__connection__ = connection
  137. self.__object_id__ = object_id
  138. if attributes is not None:
  139. self.update_remote_data(attributes)
  140. def __repr__(self): # RemoteObject
  141. txt = "<%s object at 0x%08X" % (self.__class__.__name__, id(self))
  142. # "sid" stands for short ID.
  143. sid = self.get_short_object_id()
  144. if sid is not None:
  145. txt += ", sid=%s" % sid
  146. return txt + ">"
  147. def __str__(self): # RemoteObject
  148. sid = self.get_short_object_id()
  149. if sid is None:
  150. return RemoteObject.__repr__(self)
  151. else:
  152. return "<%s, sid=%s>" % (self.__class__.__name__, sid)
  153. def get_short_object_id(self):
  154. if self.__object_id__ is None:
  155. return None
  156. return dopal.utils.make_short_object_id(self.__object_id__)
  157. def get_object_id(self): # RemoteObject
  158. return self.__object_id__
  159. def get_remote_type(self): # RemoteObject
  160. if not hasattr(self, 'get_xml_type'):
  161. return None
  162. return self.get_xml_type()
  163. def get_remote_attributes(self): # RemoteObject
  164. result = {}
  165. result['__connection__'] = self.__connection__
  166. result['__connection_id__'] = self.__connection_id__
  167. result['__object_id__'] = self.__object_id__
  168. return result
  169. # set_remote_attribute and update_remote_data are very closely
  170. # linked - the former sets one attribute at a time while the
  171. # other sets multiple attributes together. It is recommended
  172. # that set_remote_attribute is not overridden, but
  173. # update_remote_data is instead. If you choose to override
  174. # set_remote_attribute, you should override update_remote_data
  175. # to use set_remote_attribute.
  176. def set_remote_attribute(self, name, value): # RemoteObject
  177. return self.update_remote_data({name: value})
  178. def update_remote_data(self, attribute_data): # RemoteObject
  179. for key, value in attribute_data.items():
  180. setattr(self, key, value)
  181. def get_connection(self): # RemoteObject
  182. if self.__connection__ is None:
  183. raise ConnectionlessObjectError, self
  184. return self.__connection__
  185. # Exits quietly if the current connection is valid.
  186. #
  187. # If it is invalid, then this object's _refresh_object method will be
  188. # called instead to retrieve a new object ID (if applicable), but only
  189. # if this object's connection is a persistent one. If not, it will raise
  190. # a StaleObjectReferenceError.
  191. def verify_connection(self): # RemoteObject
  192. return self.get_connection().verify_objects([self])
  193. def refresh_object(self): # RemoteObject
  194. '''
  195. Updates the remote attributes on this object.
  196. @raise NonRefreshableConnectionlessObjectError: If the object is not
  197. attached to a connection.
  198. @return: None
  199. '''
  200. try:
  201. if not self.verify_connection():
  202. self._refresh_object(self.__connection__)
  203. except ConnectionlessObjectError:
  204. raise NonRefreshableConnectionlessObjectError, self
  205. def _refresh_object(self, connection_to_use): # RemoteObject
  206. '''
  207. Internal method which refreshes the attributes on the object.
  208. This method actually performs two different functionalities.
  209. If the connection to use is the same as the one already attached,
  210. with the same connection ID, then a refresh will take place.
  211. If the connection is either a different connection, or the connection
  212. ID is different, then an attempt will be made to retrieve the
  213. equivalent object to update the attributes.
  214. @param connection_to_use: The connection object to update with.
  215. @type connection_to_use: L{AzureusObjectConnection}
  216. @raise NonRefreshableObjectTypeError: Raised when the object type is
  217. not one which can be refreshed on broken connections.
  218. @raise NonRefreshableIncompleteObjectError: Raised when the object is
  219. missing certain attributes which prevents it being refreshed on broken
  220. connections.
  221. @return: None
  222. '''
  223. # If the object is still valid, let's use the refresh method.
  224. if (self.__connection__ == connection_to_use) and \
  225. self.__connection_id__ == connection_to_use.connection_id:
  226. new_object = connection_to_use.invoke_object_method(
  227. self, '_refresh', (), result_type=self.get_xml_type())
  228. # The object is no longer valid. Let's grab the equivalent object.
  229. else:
  230. # Special case - if the object is the cached plugin interface
  231. # object, then we need to avoid calling get_plugin_interface.
  232. #
  233. # Why? Because that'll pick up that the object is invalid, and
  234. # then attempt to refresh it. Recursive infinite loop.
  235. #
  236. # So in that case, we just get a plugin interface object
  237. # directly.
  238. if connection_to_use.cached_plugin_interface_object is self:
  239. new_object = connection_to_use.getPluginInterface()
  240. else:
  241. root = connection_to_use.get_plugin_interface()
  242. new_object = self._get_self_from_root_object(root)
  243. del root
  244. # Get the attributes...
  245. new_data = new_object.get_remote_attributes()
  246. # (Make sure that the important attributes are there...)
  247. if __debug__:
  248. attrs = ['__connection__', '__connection_id__', '__object_id__']
  249. for key in attrs:
  250. if key not in new_data:
  251. err = "%r.get_remote_attributes() is missing values!"
  252. raise AssertionError, err % self
  253. del attrs, key
  254. # Update the values.
  255. self.update_remote_data(new_data)
  256. # This method is used to locate the remote equivalent object from the
  257. # plugin interface object. If the object cannot be retrieved from the
  258. # PluginInterface, you should raise a NonRefreshableObjectTypeError
  259. # instead (this is the default behaviour).
  260. def _get_self_from_root_object(self, plugin_interface): # RemoteObject
  261. raise NonRefreshableObjectError, self
  262. def invoke_object_method(self, method, method_args, result_type=None): # RemoteObject
  263. try:
  264. return self.get_connection().invoke_object_method(self, method, method_args, result_type=result_type)
  265. except RemoteMethodError, error:
  266. # There's three different ways an error can be generated here:
  267. # 1) _handle_invocation_error raises an error - this will have
  268. # the traceback of where it was raised.
  269. # 2) _handle_invocation_error returns an error object - this
  270. # will have the traceback of the original exception.
  271. # 3) _handle_invocation_error returns None - this will just
  272. # reraise the original error.
  273. error = self._handle_invocation_error(error, method, method_args)
  274. if error is not None:
  275. import sys
  276. raise error, None, sys.exc_info()[2]
  277. raise
  278. def _handle_invocation_error(self, error, method_name, method_args): # RemoteObject
  279. # Default behaviour - just reraise the old error.
  280. return None
  281. # Called by the converter classes to determine the type of a remote
  282. # attribute.
  283. def _get_type_for_attribute(self, attrib_name, mapping_key=None): # RemoteObject
  284. return None
  285. class RemoteConstantsMetaclass(type):
  286. def __init__(cls, name, bases, cls_dict):
  287. super(RemoteConstantsMetaclass, cls).__init__(name, bases, cls_dict)
  288. if hasattr(cls, '__az_constants__'):
  289. for key, value in cls.__az_constants__.items():
  290. setattr(cls, key, value)
  291. # This just used for interrogation purposes - the guts of this function will
  292. # be used to build other functions (see below).
  293. #
  294. # Poor little function - ends up being consumed and tossed aside, like a
  295. # Hollow devouring a human soul.
  296. def _invoke_remote_method(self, *args, **kwargs):
  297. from dopal.utils import handle_kwargs
  298. kwargs = handle_kwargs(kwargs, result_type=None)
  299. return self.invoke_object_method(__funcname__, args, **kwargs)
  300. from dopal.utils import MethodFactory
  301. _methodobj = MethodFactory(_invoke_remote_method)
  302. make_instance_remote_method = _methodobj.make_instance_method
  303. make_class_remote_method = _methodobj.make_class_method
  304. del _methodobj, MethodFactory
  305. from dopal.aztypes import AzureusMethods
  306. class RemoteMethodMetaclass(type):
  307. def __init__(cls, name, bases, cls_dict):
  308. super(RemoteMethodMetaclass, cls).__init__(name, bases, cls_dict)
  309. az_key = '__az_methods__'
  310. if az_key not in cls_dict:
  311. methodsobj = AzureusMethods()
  312. for base in bases:
  313. if hasattr(base, az_key):
  314. methodsobj.update(getattr(base, az_key))
  315. setattr(cls, az_key, methodsobj)
  316. else:
  317. methodsobj = getattr(cls, az_key)
  318. # Create the real methods based on those in __az_methods__.
  319. for method_name in methodsobj.get_method_names():
  320. if not hasattr(cls, method_name):
  321. _mobj = make_class_remote_method(method_name, cls)
  322. setattr(cls, method_name, _mobj)
  323. class RemoteMethodMixin(object):
  324. __use_dynamic_methods__ = False
  325. __use_type_checking__ = True
  326. def __getattr__(self, name):
  327. # Anything which starts with an underscore is unlikely to be a public
  328. # method.
  329. if (not name.startswith('_')) and self.__use_dynamic_methods__:
  330. return self._get_remote_method_on_demand(name)
  331. _superclass = super(RemoteMethodMixin, self)
  332. # Influenced by code here:
  333. # http://aspn.activestate.com/ASPN/Mail/Message/python-list/1620146
  334. #
  335. # The problem is that we can't use the super object to get a
  336. # __getattr__ method for the appropriate class.
  337. self_mro = list(self.__class__.__mro__)
  338. for cls in self_mro[self_mro.index(RemoteMethodMixin)+1:]:
  339. if hasattr(cls, '__getattr__'):
  340. return cls.__getattr__(self, name)
  341. else:
  342. # Isn't there something I can call to fall back on default
  343. # behaviour?
  344. text = "'%s' object has no attribute '%s'"
  345. raise AttributeError, text % (type(self).__name__, name)
  346. # Used to create a remote method object on demand.
  347. def _get_remote_method_on_demand(self, name):
  348. return make_instance_remote_method(name, self)
  349. def invoke_object_method(self, method, method_args, result_type=None):
  350. if self.__use_type_checking__:
  351. try:
  352. az_methods = self.__az_methods__
  353. except AttributeError:
  354. if not self.__use_dynamic_methods__:
  355. raise RuntimeError, "%s uses type checking, but has no methods to check against" % type(self).__name__
  356. else:
  357. try:
  358. method_args, result_type = \
  359. az_methods.wrap_args(method, method_args)
  360. except AzMethodError:
  361. if not self.__use_dynamic_methods__:
  362. raise
  363. return super(RemoteMethodMixin, self).invoke_object_method(method, method_args, result_type=result_type)
  364. class RemoteAttributeMetaclass(type):
  365. # XXX: What the hell is this meant to do!?
  366. def __init__(cls, name, bases, cls_dict):
  367. deft_names = '__default_remote_attribute_names__'
  368. az_attrs = '__az_attributes__'
  369. attr_dict = cls_dict.setdefault(deft_names, {})
  370. attr_dict.update(cls_dict.get(az_attrs, {}))
  371. for base in bases:
  372. attr_dict.update(getattr(base, deft_names, {}))
  373. attr_dict.update(getattr(base, az_attrs, {}))
  374. setattr(cls, deft_names, attr_dict)
  375. super(RemoteAttributeMetaclass, cls).__init__(name, bases, cls_dict)
  376. class RemoteAttributesMixin(object):
  377. __default_remote_attribute_names__ = {}
  378. __reset_attributes_on_refresh__ = False
  379. __protect_remote_attributes__ = True
  380. def __init__(self, *args, **kwargs):
  381. # Class attribute becomes instance attribute.
  382. super(RemoteAttributesMixin, self).__init__(*args, **kwargs)
  383. self.__remote_attribute_names__ = self.__default_remote_attribute_names__.copy()
  384. def __getattr__(self, name):
  385. if name in self.__remote_attribute_names__:
  386. raise MissingRemoteAttributeError, name
  387. # Influenced by code here:
  388. # http://aspn.activestate.com/ASPN/Mail/Message/python-list/1620146
  389. self_mro = list(self.__class__.__mro__)
  390. for cls in self_mro[self_mro.index(RemoteAttributesMixin)+1:]:
  391. if hasattr(cls, '__getattr__'):
  392. return cls.__getattr__(self, name)
  393. else:
  394. # Isn't there something I can call to fall back on default
  395. # behaviour?
  396. text = "'%s' object has no attribute '%s'"
  397. raise AttributeError, text % (type(self).__name__, name)
  398. def __setattr__(self, name, value):
  399. if self.__protect_remote_attributes__ and not name.startswith('__'):
  400. if name in self.__remote_attribute_names__:
  401. err = "cannot set remote attribute directly: %s"
  402. raise AttributeError, err % name
  403. return super(RemoteAttributesMixin, self).__setattr__(name, value)
  404. def set_remote_attribute(self, name, value):
  405. if name not in self.__remote_attribute_names__:
  406. self.__remote_attribute_names__[name] = None
  407. return super(RemoteAttributesMixin, self).__setattr__(name, value)
  408. def get_remote_attributes(self):
  409. result = super(RemoteAttributesMixin, self).get_remote_attributes()
  410. for attribute in self.__remote_attribute_names__:
  411. if hasattr(self, attribute):
  412. result[attribute] = getattr(self, attribute)
  413. return result
  414. def is_remote_attribute(self, name):
  415. return name in self.__remote_attribute_names__
  416. def update_remote_data(self, remote_attribute_dict):
  417. if self.__reset_attributes_on_refresh__:
  418. for attrib in self.__remote_attribute_names__:
  419. try:
  420. delattr(self, attrib)
  421. except AttributeError:
  422. pass
  423. _super = super(RemoteAttributesMixin, self)
  424. # XXX: Do a better fix than this!
  425. pra_value = self.__protect_remote_attributes__
  426. self.__protect_remote_attributes__ = False
  427. try:
  428. return _super.update_remote_data(remote_attribute_dict)
  429. finally:
  430. self.__protect_remote_attributes__ = pra_value
  431. def _get_type_for_attribute(self, name, mapping_key=None):
  432. if mapping_key is not None:
  433. key_to_use = name + ',' + mapping_key
  434. else:
  435. key_to_use = name
  436. result = self.__remote_attribute_names__.get(key_to_use)
  437. if result is not None:
  438. return result
  439. else:
  440. import dopal
  441. if dopal.__dopal_mode__ == 1:
  442. raise RuntimeError, (self, key_to_use)
  443. _superfunc = super(RemoteAttributesMixin, self)._get_type_for_attribute
  444. return _superfunc(name, mapping_key)
  445. class AzureusObjectMetaclass(RemoteConstantsMetaclass, RemoteMethodMetaclass, RemoteAttributeMetaclass):
  446. pass
  447. class AzureusObject(RemoteAttributesMixin, RemoteMethodMixin, RemoteObject):
  448. __metaclass__ = AzureusObjectMetaclass
  449. def _get_self_from_root_object(self, plugin_interface):
  450. # XXX: Err, this is a bit incorrect - it should be get_remote_type.
  451. # But it will do for now. Need to think more carefully about
  452. # the responsibilities of the two methods.
  453. if hasattr(self, 'get_xml_type'):
  454. from dopal.persistency import get_equivalent_object_from_root
  455. return get_equivalent_object_from_root(self, plugin_interface)
  456. return super(AzureusObject, self)._get_self_from_root_object(plugin_interface)
  457. class TypelessRemoteObject(RemoteAttributesMixin, RemoteMethodMixin, RemoteObject):
  458. __use_dynamic_methods__ = True
  459. TYPELESS_CLASS_MAP = {None: TypelessRemoteObject}
  460. # XXX: Define converter here?
  461. # Add type checking code (though this proably should be core)
  462. # Add some code to read data from statistics file (what level should this be at?)
  463. ## Allow some code to make link_error_handler assignable
  464. # Converter - needs to have some default behaviours (easily changeable):
  465. # a) Atoms - what to do if no type is suggested. (not so important this one)
  466. # b) Objects - what to do if no class is given (if no id is given?)