1 # -*- test-case-name: twisted.test.test_pb -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
6 This module represents flavors of remotely acessible objects.
8 Currently this is only objects accessible through Perspective Broker, but will
9 hopefully encompass all forms of remote access which can emulate subsets of PB
10 (such as XMLRPC or SOAP).
12 Future Plans: Optimization. Exploitation of new-style object model.
13 Optimizations to this module should not affect external-use semantics at all,
14 but may have a small impact on users who subclass and override methods.
16 @author: Glyph Lefkowitz
19 # NOTE: this module should NOT import pb; it is supposed to be a module which
20 # abstractly defines remotely accessible types. Many of these types expect to
21 # be serialized by Jelly, but they ought to be accessible through other
22 # mechanisms (like XMLRPC)
26 from zope.interface import implements, Interface
29 from twisted.python import log, reflect
32 from jelly import setUnjellyableForClass, setUnjellyableForClassTree, setUnjellyableFactoryForClass, unjellyableRegistry
33 from jelly import Jellyable, Unjellyable, _newDummyLike
34 from jelly import setInstanceState, getInstanceState
37 setCopierForClass = setUnjellyableForClass
38 setCopierForClassTree = setUnjellyableForClassTree
39 setFactoryForClass = setUnjellyableFactoryForClass
40 copyTags = unjellyableRegistry
44 cached_atom = "cached"
45 remote_atom = "remote"
48 class NoSuchMethod(AttributeError):
49 """Raised if there is no such remote method"""
52 class IPBRoot(Interface):
53 """Factory for root Referenceable objects for PB servers."""
55 def rootObject(broker):
56 """Return root Referenceable for broker."""
59 class Serializable(Jellyable):
60 """An object that can be passed remotely.
62 I am a style of object which can be serialized by Perspective
63 Broker. Objects which wish to be referenceable or copied remotely
64 have to subclass Serializable. However, clients of Perspective
65 Broker will probably not want to directly subclass Serializable; the
66 Flavors of transferable objects are listed below.
68 What it means to be \"Serializable\" is that an object can be
69 passed to or returned from a remote method. Certain basic types
70 (dictionaries, lists, tuples, numbers, strings) are serializable by
71 default; however, classes need to choose a specific serialization
72 style: L{Referenceable}, L{Viewable}, L{Copyable} or L{Cacheable}.
74 You may also pass C{[lists, dictionaries, tuples]} of L{Serializable}
75 instances to or return them from remote methods, as many levels deep
79 def processUniqueID(self):
80 """Return an ID which uniquely represents this object for this process.
82 By default, this uses the 'id' builtin, but can be overridden to
83 indicate that two values are identity-equivalent (such as proxies
89 class Referenceable(Serializable):
91 """I am an object sent remotely as a direct reference.
93 When one of my subclasses is sent as an argument to or returned
94 from a remote method call, I will be serialized by default as a
97 This means that the peer will be able to call methods on me;
98 a method call xxx() from my peer will be resolved to methods
99 of the name remote_xxx.
102 def remoteMessageReceived(self, broker, message, args, kw):
103 """A remote message has been received. Dispatch it appropriately.
105 The default implementation is to dispatch to a method called
106 'remote_messagename' and call it with the same arguments.
108 args = broker.unserialize(args)
109 kw = broker.unserialize(kw)
110 method = getattr(self, "remote_%s" % message, None)
112 raise NoSuchMethod("No such method: remote_%s" % (message,))
114 state = method(*args, **kw)
116 log.msg("%s didn't accept %s and %s" % (method, args, kw))
118 return broker.serialize(state, self.perspective)
120 def jellyFor(self, jellier):
123 Return a tuple which will be used as the s-expression to
124 serialize this to a peer.
127 return ["remote", jellier.invoker.registerReference(self)]
130 class Root(Referenceable):
131 """I provide a root object to L{pb.Broker}s for a L{pb.BrokerFactory}.
133 When a L{pb.BrokerFactory} produces a L{pb.Broker}, it supplies that
134 L{pb.Broker} with an object named \"root\". That object is obtained
135 by calling my rootObject method.
140 def rootObject(self, broker):
141 """A L{pb.BrokerFactory} is requesting to publish me as a root object.
143 When a L{pb.BrokerFactory} is sending me as the root object, this
144 method will be invoked to allow per-broker versions of an
145 object. By default I return myself.
150 class ViewPoint(Referenceable):
152 I act as an indirect reference to an object accessed through a
155 Simply put, I combine an object with a perspective so that when a
156 peer calls methods on the object I refer to, the method will be
157 invoked with that perspective as a first argument, so that it can
158 know who is calling it.
160 While L{Viewable} objects will be converted to ViewPoints by default
161 when they are returned from or sent as arguments to a remote
162 method, any object may be manually proxied as well. (XXX: Now that
163 this class is no longer named C{Proxy}, this is the only occourance
164 of the term 'proxied' in this docstring, and may be unclear.)
166 This can be useful when dealing with L{pb.Perspective}s, L{Copyable}s,
167 and L{Cacheable}s. It is legal to implement a method as such on
170 | def perspective_getViewPointForOther(self, name):
171 | defr = self.service.getPerspectiveRequest(name)
172 | defr.addCallbacks(lambda x, self=self: ViewPoint(self, x), log.msg)
175 This will allow you to have references to Perspective objects in two
176 different ways. One is through the initial 'attach' call -- each
177 peer will have a L{pb.RemoteReference} to their perspective directly. The
178 other is through this method; each peer can get a L{pb.RemoteReference} to
179 all other perspectives in the service; but that L{pb.RemoteReference} will
180 be to a L{ViewPoint}, not directly to the object.
182 The practical offshoot of this is that you can implement 2 varieties
183 of remotely callable methods on this Perspective; view_xxx and
184 C{perspective_xxx}. C{view_xxx} methods will follow the rules for
185 ViewPoint methods (see ViewPoint.L{remoteMessageReceived}), and
186 C{perspective_xxx} methods will follow the rules for Perspective
190 def __init__(self, perspective, object):
191 """Initialize me with a Perspective and an Object.
193 self.perspective = perspective
196 def processUniqueID(self):
197 """Return an ID unique to a proxy for this perspective+object combination.
199 return (id(self.perspective), id(self.object))
201 def remoteMessageReceived(self, broker, message, args, kw):
202 """A remote message has been received. Dispatch it appropriately.
204 The default implementation is to dispatch to a method called
205 'C{view_messagename}' to my Object and call it on my object with
206 the same arguments, modified by inserting my Perspective as
209 args = broker.unserialize(args, self.perspective)
210 kw = broker.unserialize(kw, self.perspective)
211 method = getattr(self.object, "view_%s" % message)
213 state = apply(method, (self.perspective,)+args, kw)
215 log.msg("%s didn't accept %s and %s" % (method, args, kw))
217 rv = broker.serialize(state, self.perspective, method, args, kw)
221 class Viewable(Serializable):
222 """I will be converted to a L{ViewPoint} when passed to or returned from a remote method.
224 The beginning of a peer's interaction with a PB Service is always
225 through a perspective. However, if a C{perspective_xxx} method returns
226 a Viewable, it will be serialized to the peer as a response to that
230 def jellyFor(self, jellier):
231 """Serialize a L{ViewPoint} for me and the perspective of the given broker.
233 return ViewPoint(jellier.invoker.serializingPerspective, self).jellyFor(jellier)
237 class Copyable(Serializable):
238 """Subclass me to get copied each time you are returned from or passed to a remote method.
240 When I am returned from or passed to a remote method call, I will be
241 converted into data via a set of callbacks (see my methods for more
242 info). That data will then be serialized using Jelly, and sent to
245 The peer will then look up the type to represent this with; see
246 L{RemoteCopy} for details.
249 def getStateToCopy(self):
250 """Gather state to send when I am serialized for a peer.
252 I will default to returning self.__dict__. Override this to
253 customize this behavior.
258 def getStateToCopyFor(self, perspective):
260 Gather state to send when I am serialized for a particular
263 I will default to calling L{getStateToCopy}. Override this to
264 customize this behavior.
267 return self.getStateToCopy()
269 def getTypeToCopy(self):
270 """Determine what type tag to send for me.
272 By default, send the string representation of my class
273 (package.module.Class); normally this is adequate, but
274 you may override this to change it.
277 return reflect.qual(self.__class__)
279 def getTypeToCopyFor(self, perspective):
280 """Determine what type tag to send for me.
282 By default, defer to self.L{getTypeToCopy}() normally this is
283 adequate, but you may override this to change it.
286 return self.getTypeToCopy()
288 def jellyFor(self, jellier):
289 """Assemble type tag and state to copy for this broker.
291 This will call L{getTypeToCopyFor} and L{getStateToCopy}, and
292 return an appropriate s-expression to represent me.
295 if jellier.invoker is None:
296 return getInstanceState(self, jellier)
297 p = jellier.invoker.serializingPerspective
298 t = self.getTypeToCopyFor(p)
299 state = self.getStateToCopyFor(p)
300 sxp = jellier.prepare(self)
301 sxp.extend([t, jellier.jelly(state)])
302 return jellier.preserve(self, sxp)
305 class Cacheable(Copyable):
306 """A cached instance.
308 This means that it's copied; but there is some logic to make sure
309 that it's only copied once. Additionally, when state is retrieved,
310 it is passed a "proto-reference" to the state as it will exist on
313 XXX: The documentation for this class needs work, but it's the most
314 complex part of PB and it is inherently difficult to explain.
317 def getStateToCacheAndObserveFor(self, perspective, observer):
319 Get state to cache on the client and client-cache reference
322 This is similiar to getStateToCopyFor, but it additionally
323 passes in a reference to the client-side RemoteCache instance
324 that will be created when it is unserialized. This allows
325 Cacheable instances to keep their RemoteCaches up to date when
326 they change, such that no changes can occur between the point
327 at which the state is initially copied and the client receives
328 it that are not propogated.
331 return self.getStateToCopyFor(perspective)
333 def jellyFor(self, jellier):
334 """Return an appropriate tuple to serialize me.
336 Depending on whether this broker has cached me or not, this may
337 return either a full state or a reference to an existing cache.
339 if jellier.invoker is None:
340 return getInstanceState(self, jellier)
341 luid = jellier.invoker.cachedRemotelyAs(self, 1)
343 luid = jellier.invoker.cacheRemotely(self)
344 p = jellier.invoker.serializingPerspective
345 type_ = self.getTypeToCopyFor(p)
346 observer = RemoteCacheObserver(jellier.invoker, self, p)
347 state = self.getStateToCacheAndObserveFor(p, observer)
348 l = jellier.prepare(self)
349 jstate = jellier.jelly(state)
350 l.extend([type_, luid, jstate])
351 return jellier.preserve(self, l)
353 return cached_atom, luid
355 def stoppedObserving(self, perspective, observer):
356 """This method is called when a client has stopped observing me.
358 The 'observer' argument is the same as that passed in to
359 getStateToCacheAndObserveFor.
364 class RemoteCopy(Unjellyable):
365 """I am a remote copy of a Copyable object.
367 When the state from a L{Copyable} object is received, an instance will
368 be created based on the copy tags table (see setUnjellyableForClass) and
369 sent the L{setCopyableState} message. I provide a reasonable default
370 implementation of that message; subclass me if you wish to serve as
371 a copier for remote data.
373 NOTE: copiers are invoked with no arguments. Do not implement a
374 constructor which requires args in a subclass of L{RemoteCopy}!
377 def setCopyableState(self, state):
378 """I will be invoked with the state to copy locally.
380 'state' is the data returned from the remote object's
381 'getStateToCopyFor' method, which will often be the remote
382 object's dictionary (or a filtered approximation of it depending
383 on my peer's perspective).
386 self.__dict__ = state
388 def unjellyFor(self, unjellier, jellyList):
389 if unjellier.invoker is None:
390 return setInstanceState(self, unjellier, jellyList)
391 self.setCopyableState(unjellier.unjelly(jellyList[1]))
396 class RemoteCache(RemoteCopy, Serializable):
397 """A cache is a local representation of a remote L{Cacheable} object.
399 This represents the last known state of this object. It may
400 also have methods invoked on it -- in order to update caches,
401 the cached class generates a L{pb.RemoteReference} to this object as
402 it is originally sent.
404 Much like copy, I will be invoked with no arguments. Do not
405 implement a constructor that requires arguments in one of my
409 def remoteMessageReceived(self, broker, message, args, kw):
410 """A remote message has been received. Dispatch it appropriately.
412 The default implementation is to dispatch to a method called
413 'C{observe_messagename}' and call it on my with the same arguments.
416 args = broker.unserialize(args)
417 kw = broker.unserialize(kw)
418 method = getattr(self, "observe_%s" % message)
420 state = apply(method, args, kw)
422 log.msg("%s didn't accept %s and %s" % (method, args, kw))
424 return broker.serialize(state, None, method, args, kw)
426 def jellyFor(self, jellier):
427 """serialize me (only for the broker I'm for) as the original cached reference
429 if jellier.invoker is None:
430 return getInstanceState(self, jellier)
431 assert jellier.invoker is self.broker, "You cannot exchange cached proxies between brokers."
432 return 'lcache', self.luid
435 def unjellyFor(self, unjellier, jellyList):
436 if unjellier.invoker is None:
437 return setInstanceState(self, unjellier, jellyList)
438 self.broker = unjellier.invoker
439 self.luid = jellyList[1]
440 cProxy = _newDummyLike(self)
441 # XXX questionable whether this was a good design idea...
442 init = getattr(cProxy, "__init__", None)
445 unjellier.invoker.cacheLocally(jellyList[1], self)
446 cProxy.setCopyableState(unjellier.unjelly(jellyList[2]))
447 # Might have changed due to setCopyableState method; we'll assume that
448 # it's bad form to do so afterwards.
449 self.__dict__ = cProxy.__dict__
450 # chomp, chomp -- some existing code uses "self.__dict__ =", some uses
451 # "__dict__.update". This is here in order to handle both cases.
452 self.broker = unjellier.invoker
453 self.luid = jellyList[1]
456 ## def __really_del__(self):
457 ## """Final finalization call, made after all remote references have been lost.
460 def __cmp__(self, other):
461 """Compare me [to another RemoteCache.
463 if isinstance(other, self.__class__):
464 return cmp(id(self.__dict__), id(other.__dict__))
466 return cmp(id(self.__dict__), other)
471 return int(id(self.__dict__) % sys.maxint)
477 """Do distributed reference counting on finalize.
480 # log.msg( ' --- decache: %s %s' % (self, self.luid) )
482 self.broker.decCacheRef(self.luid)
486 def unjellyCached(unjellier, unjellyList):
487 luid = unjellyList[1]
488 cNotProxy = unjellier.invoker.cachedLocallyAs(luid)
489 cProxy = _newDummyLike(cNotProxy)
492 setUnjellyableForClass("cached", unjellyCached)
494 def unjellyLCache(unjellier, unjellyList):
495 luid = unjellyList[1]
496 obj = unjellier.invoker.remotelyCachedForLUID(luid)
499 setUnjellyableForClass("lcache", unjellyLCache)
501 def unjellyLocal(unjellier, unjellyList):
502 obj = unjellier.invoker.localObjectForID(unjellyList[1])
505 setUnjellyableForClass("local", unjellyLocal)
507 class RemoteCacheMethod:
508 """A method on a reference to a L{RemoteCache}.
511 def __init__(self, name, broker, cached, perspective):
512 """(internal) initialize.
516 self.perspective = perspective
519 def __cmp__(self, other):
520 return cmp((self.name, self.broker, self.perspective, self.cached), other)
523 return hash((self.name, self.broker, self.perspective, self.cached))
525 def __call__(self, *args, **kw):
526 """(internal) action method.
528 cacheID = self.broker.cachedRemotelyAs(self.cached)
530 from pb import ProtocolError
531 raise ProtocolError("You can't call a cached method when the object hasn't been given to the peer yet.")
532 return self.broker._sendMessage('cache', self.perspective, cacheID, self.name, args, kw)
534 class RemoteCacheObserver:
535 """I am a reverse-reference to the peer's L{RemoteCache}.
537 I am generated automatically when a cache is serialized. I
538 represent a reference to the client's L{RemoteCache} object that
539 will represent a particular L{Cacheable}; I am the additional
540 object passed to getStateToCacheAndObserveFor.
543 def __init__(self, broker, cached, perspective):
544 """(internal) Initialize me.
546 @param broker: a L{pb.Broker} instance.
548 @param cached: a L{Cacheable} instance that this L{RemoteCacheObserver}
551 @param perspective: a reference to the perspective who is observing this.
556 self.perspective = perspective
559 return "<RemoteCacheObserver(%s, %s, %s) at %s>" % (
560 self.broker, self.cached, self.perspective, id(self))
563 """Generate a hash unique to all L{RemoteCacheObserver}s for this broker/perspective/cached triplet
566 return ( (hash(self.broker) % 2**10)
567 + (hash(self.perspective) % 2**10)
568 + (hash(self.cached) % 2**10))
570 def __cmp__(self, other):
571 """Compare me to another L{RemoteCacheObserver}.
574 return cmp((self.broker, self.perspective, self.cached), other)
576 def callRemote(self, _name, *args, **kw):
577 """(internal) action method.
579 cacheID = self.broker.cachedRemotelyAs(self.cached)
581 from pb import ProtocolError
582 raise ProtocolError("You can't call a cached method when the "
583 "object hasn't been given to the peer yet.")
584 return self.broker._sendMessage('cache', self.perspective, cacheID,
587 def remoteMethod(self, key):
588 """Get a L{pb.RemoteMethod} for this key.
590 return RemoteCacheMethod(key, self.broker, self.cached, self.perspective)