1
# -*- test-case-name: twisted.test.test_pb -*-
2
# Copyright (c) 2001-2008 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, _Dummy, _DummyNewStyle
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.
137
See also: L{pb.getObjectAt}
142
def rootObject(self, broker):
143
"""A L{pb.BrokerFactory} is requesting to publish me as a root object.
145
When a L{pb.BrokerFactory} is sending me as the root object, this
146
method will be invoked to allow per-broker versions of an
147
object. By default I return myself.
152
class ViewPoint(Referenceable):
154
I act as an indirect reference to an object accessed through a
157
Simply put, I combine an object with a perspective so that when a
158
peer calls methods on the object I refer to, the method will be
159
invoked with that perspective as a first argument, so that it can
160
know who is calling it.
162
While L{Viewable} objects will be converted to ViewPoints by default
163
when they are returned from or sent as arguments to a remote
164
method, any object may be manually proxied as well. (XXX: Now that
165
this class is no longer named C{Proxy}, this is the only occourance
166
of the term 'proxied' in this docstring, and may be unclear.)
168
This can be useful when dealing with L{pb.Perspective}s, L{Copyable}s,
169
and L{Cacheable}s. It is legal to implement a method as such on
172
| def perspective_getViewPointForOther(self, name):
173
| defr = self.service.getPerspectiveRequest(name)
174
| defr.addCallbacks(lambda x, self=self: ViewPoint(self, x), log.msg)
177
This will allow you to have references to Perspective objects in two
178
different ways. One is through the initial 'attach' call -- each
179
peer will have a L{pb.RemoteReference} to their perspective directly. The
180
other is through this method; each peer can get a L{pb.RemoteReference} to
181
all other perspectives in the service; but that L{pb.RemoteReference} will
182
be to a L{ViewPoint}, not directly to the object.
184
The practical offshoot of this is that you can implement 2 varieties
185
of remotely callable methods on this Perspective; view_xxx and
186
C{perspective_xxx}. C{view_xxx} methods will follow the rules for
187
ViewPoint methods (see ViewPoint.L{remoteMessageReceived}), and
188
C{perspective_xxx} methods will follow the rules for Perspective
192
def __init__(self, perspective, object):
193
"""Initialize me with a Perspective and an Object.
195
self.perspective = perspective
198
def processUniqueID(self):
199
"""Return an ID unique to a proxy for this perspective+object combination.
201
return (id(self.perspective), id(self.object))
203
def remoteMessageReceived(self, broker, message, args, kw):
204
"""A remote message has been received. Dispatch it appropriately.
206
The default implementation is to dispatch to a method called
207
'C{view_messagename}' to my Object and call it on my object with
208
the same arguments, modified by inserting my Perspective as
211
args = broker.unserialize(args, self.perspective)
212
kw = broker.unserialize(kw, self.perspective)
213
method = getattr(self.object, "view_%s" % message)
215
state = apply(method, (self.perspective,)+args, kw)
217
log.msg("%s didn't accept %s and %s" % (method, args, kw))
219
rv = broker.serialize(state, self.perspective, method, args, kw)
223
class Viewable(Serializable):
224
"""I will be converted to a L{ViewPoint} when passed to or returned from a remote method.
226
The beginning of a peer's interaction with a PB Service is always
227
through a perspective. However, if a C{perspective_xxx} method returns
228
a Viewable, it will be serialized to the peer as a response to that
232
def jellyFor(self, jellier):
233
"""Serialize a L{ViewPoint} for me and the perspective of the given broker.
235
return ViewPoint(jellier.invoker.serializingPerspective, self).jellyFor(jellier)
239
class Copyable(Serializable):
240
"""Subclass me to get copied each time you are returned from or passed to a remote method.
242
When I am returned from or passed to a remote method call, I will be
243
converted into data via a set of callbacks (see my methods for more
244
info). That data will then be serialized using Jelly, and sent to
247
The peer will then look up the type to represent this with; see
248
L{RemoteCopy} for details.
251
def getStateToCopy(self):
252
"""Gather state to send when I am serialized for a peer.
254
I will default to returning self.__dict__. Override this to
255
customize this behavior.
260
def getStateToCopyFor(self, perspective):
262
Gather state to send when I am serialized for a particular
265
I will default to calling L{getStateToCopy}. Override this to
266
customize this behavior.
269
return self.getStateToCopy()
271
def getTypeToCopy(self):
272
"""Determine what type tag to send for me.
274
By default, send the string representation of my class
275
(package.module.Class); normally this is adequate, but
276
you may override this to change it.
279
return reflect.qual(self.__class__)
281
def getTypeToCopyFor(self, perspective):
282
"""Determine what type tag to send for me.
284
By default, defer to self.L{getTypeToCopy}() normally this is
285
adequate, but you may override this to change it.
288
return self.getTypeToCopy()
290
def jellyFor(self, jellier):
291
"""Assemble type tag and state to copy for this broker.
293
This will call L{getTypeToCopyFor} and L{getStateToCopy}, and
294
return an appropriate s-expression to represent me.
297
if jellier.invoker is None:
298
return getInstanceState(self, jellier)
299
p = jellier.invoker.serializingPerspective
300
t = self.getTypeToCopyFor(p)
301
state = self.getStateToCopyFor(p)
302
sxp = jellier.prepare(self)
303
sxp.extend([t, jellier.jelly(state)])
304
return jellier.preserve(self, sxp)
307
class Cacheable(Copyable):
308
"""A cached instance.
310
This means that it's copied; but there is some logic to make sure
311
that it's only copied once. Additionally, when state is retrieved,
312
it is passed a "proto-reference" to the state as it will exist on
315
XXX: The documentation for this class needs work, but it's the most
316
complex part of PB and it is inherently difficult to explain.
319
def getStateToCacheAndObserveFor(self, perspective, observer):
321
Get state to cache on the client and client-cache reference
324
This is similiar to getStateToCopyFor, but it additionally
325
passes in a reference to the client-side RemoteCache instance
326
that will be created when it is unserialized. This allows
327
Cacheable instances to keep their RemoteCaches up to date when
328
they change, such that no changes can occur between the point
329
at which the state is initially copied and the client receives
330
it that are not propogated.
333
return self.getStateToCopyFor(perspective)
335
def jellyFor(self, jellier):
336
"""Return an appropriate tuple to serialize me.
338
Depending on whether this broker has cached me or not, this may
339
return either a full state or a reference to an existing cache.
341
if jellier.invoker is None:
342
return getInstanceState(self, jellier)
343
luid = jellier.invoker.cachedRemotelyAs(self, 1)
345
luid = jellier.invoker.cacheRemotely(self)
346
p = jellier.invoker.serializingPerspective
347
type_ = self.getTypeToCopyFor(p)
348
observer = RemoteCacheObserver(jellier.invoker, self, p)
349
state = self.getStateToCacheAndObserveFor(p, observer)
350
l = jellier.prepare(self)
351
jstate = jellier.jelly(state)
352
l.extend([type_, luid, jstate])
353
return jellier.preserve(self, l)
355
return cached_atom, luid
357
def stoppedObserving(self, perspective, observer):
358
"""This method is called when a client has stopped observing me.
360
The 'observer' argument is the same as that passed in to
361
getStateToCacheAndObserveFor.
366
class RemoteCopy(Unjellyable):
367
"""I am a remote copy of a Copyable object.
369
When the state from a L{Copyable} object is received, an instance will
370
be created based on the copy tags table (see setUnjellyableForClass) and
371
sent the L{setCopyableState} message. I provide a reasonable default
372
implementation of that message; subclass me if you wish to serve as
373
a copier for remote data.
375
NOTE: copiers are invoked with no arguments. Do not implement a
376
constructor which requires args in a subclass of L{RemoteCopy}!
379
def setCopyableState(self, state):
380
"""I will be invoked with the state to copy locally.
382
'state' is the data returned from the remote object's
383
'getStateToCopyFor' method, which will often be the remote
384
object's dictionary (or a filtered approximation of it depending
385
on my peer's perspective).
388
self.__dict__ = state
390
def unjellyFor(self, unjellier, jellyList):
391
if unjellier.invoker is None:
392
return setInstanceState(self, unjellier, jellyList)
393
self.setCopyableState(unjellier.unjelly(jellyList[1]))
398
class RemoteCache(RemoteCopy, Serializable):
399
"""A cache is a local representation of a remote L{Cacheable} object.
401
This represents the last known state of this object. It may
402
also have methods invoked on it -- in order to update caches,
403
the cached class generates a L{pb.RemoteReference} to this object as
404
it is originally sent.
406
Much like copy, I will be invoked with no arguments. Do not
407
implement a constructor that requires arguments in one of my
411
def remoteMessageReceived(self, broker, message, args, kw):
412
"""A remote message has been received. Dispatch it appropriately.
414
The default implementation is to dispatch to a method called
415
'C{observe_messagename}' and call it on my with the same arguments.
418
args = broker.unserialize(args)
419
kw = broker.unserialize(kw)
420
method = getattr(self, "observe_%s" % message)
422
state = apply(method, args, kw)
424
log.msg("%s didn't accept %s and %s" % (method, args, kw))
426
return broker.serialize(state, None, method, args, kw)
428
def jellyFor(self, jellier):
429
"""serialize me (only for the broker I'm for) as the original cached reference
431
if jellier.invoker is None:
432
return getInstanceState(self, jellier)
433
assert jellier.invoker is self.broker, "You cannot exchange cached proxies between brokers."
434
return 'lcache', self.luid
437
def unjellyFor(self, unjellier, jellyList):
438
if unjellier.invoker is None:
439
return setInstanceState(self, unjellier, jellyList)
440
self.broker = unjellier.invoker
441
self.luid = jellyList[1]
442
if isinstance(self.__class__, type): #new-style class
443
cProxy = _DummyNewStyle()
446
cProxy.__class__ = self.__class__
447
cProxy.__dict__ = self.__dict__
448
# XXX questionable whether this was a good design idea...
449
init = getattr(cProxy, "__init__", None)
452
unjellier.invoker.cacheLocally(jellyList[1], self)
453
cProxy.setCopyableState(unjellier.unjelly(jellyList[2]))
454
# Might have changed due to setCopyableState method; we'll assume that
455
# it's bad form to do so afterwards.
456
self.__dict__ = cProxy.__dict__
457
# chomp, chomp -- some existing code uses "self.__dict__ =", some uses
458
# "__dict__.update". This is here in order to handle both cases.
459
self.broker = unjellier.invoker
460
self.luid = jellyList[1]
463
## def __really_del__(self):
464
## """Final finalization call, made after all remote references have been lost.
467
def __cmp__(self, other):
468
"""Compare me [to another RemoteCache.
470
if isinstance(other, self.__class__):
471
return cmp(id(self.__dict__), id(other.__dict__))
473
return cmp(id(self.__dict__), other)
478
return int(id(self.__dict__) % sys.maxint)
484
"""Do distributed reference counting on finalize.
487
# log.msg( ' --- decache: %s %s' % (self, self.luid) )
489
self.broker.decCacheRef(self.luid)
493
def unjellyCached(unjellier, unjellyList):
494
luid = unjellyList[1]
495
cNotProxy = unjellier.invoker.cachedLocallyAs(luid)
498
cProxy.__class__ = cNotProxy.__class__
499
cProxy.__dict__ = cNotProxy.__dict__
502
setUnjellyableForClass("cached", unjellyCached)
504
def unjellyLCache(unjellier, unjellyList):
505
luid = unjellyList[1]
506
obj = unjellier.invoker.remotelyCachedForLUID(luid)
509
setUnjellyableForClass("lcache", unjellyLCache)
511
def unjellyLocal(unjellier, unjellyList):
512
obj = unjellier.invoker.localObjectForID(unjellyList[1])
515
setUnjellyableForClass("local", unjellyLocal)
517
class RemoteCacheMethod:
518
"""A method on a reference to a L{RemoteCache}.
521
def __init__(self, name, broker, cached, perspective):
522
"""(internal) initialize.
526
self.perspective = perspective
529
def __cmp__(self, other):
530
return cmp((self.name, self.broker, self.perspective, self.cached), other)
533
return hash((self.name, self.broker, self.perspective, self.cached))
535
def __call__(self, *args, **kw):
536
"""(internal) action method.
538
cacheID = self.broker.cachedRemotelyAs(self.cached)
540
from pb import ProtocolError
541
raise ProtocolError("You can't call a cached method when the object hasn't been given to the peer yet.")
542
return self.broker._sendMessage('cache', self.perspective, cacheID, self.name, args, kw)
544
class RemoteCacheObserver:
545
"""I am a reverse-reference to the peer's L{RemoteCache}.
547
I am generated automatically when a cache is serialized. I
548
represent a reference to the client's L{RemoteCache} object that
549
will represent a particular L{Cacheable}; I am the additional
550
object passed to getStateToCacheAndObserveFor.
553
def __init__(self, broker, cached, perspective):
554
"""(internal) Initialize me.
556
@param broker: a L{pb.Broker} instance.
558
@param cached: a L{Cacheable} instance that this L{RemoteCacheObserver}
561
@param perspective: a reference to the perspective who is observing this.
566
self.perspective = perspective
569
return "<RemoteCacheObserver(%s, %s, %s) at %s>" % (
570
self.broker, self.cached, self.perspective, id(self))
573
"""Generate a hash unique to all L{RemoteCacheObserver}s for this broker/perspective/cached triplet
576
return ( (hash(self.broker) % 2**10)
577
+ (hash(self.perspective) % 2**10)
578
+ (hash(self.cached) % 2**10))
580
def __cmp__(self, other):
581
"""Compare me to another L{RemoteCacheObserver}.
584
return cmp((self.broker, self.perspective, self.cached), other)
586
def callRemote(self, _name, *args, **kw):
587
"""(internal) action method.
589
cacheID = self.broker.cachedRemotelyAs(self.cached)
591
from pb import ProtocolError
592
raise ProtocolError("You can't call a cached method when the "
593
"object hasn't been given to the peer yet.")
594
return self.broker._sendMessage('cache', self.perspective, cacheID,
597
def remoteMethod(self, key):
598
"""Get a L{pb.RemoteMethod} for this key.
600
return RemoteCacheMethod(key, self.broker, self.cached, self.perspective)