def _parseRemoteInterface(self, iname, attrs): remote_attrs = {} remote_name = attrs.get("__remote_name__", iname) # and see if there is a __remote_name__ . We delete it because # InterfaceClass doesn't like arbitrary attributes if attrs.has_key("__remote_name__"): del attrs["__remote_name__"] # determine all remotely-callable methods names = [ name for name in attrs.keys() if ( (type(attrs[name]) == types.FunctionType and not name.startswith("_")) or IConstraint.providedBy(attrs[name]) ) ] # turn them into constraints. Tag each of them with their name and # the RemoteInterface they came from. for name in names: m = attrs[name] if not IConstraint.providedBy(m): m = RemoteMethodSchema(method=m) m.name = name m.interface = self remote_attrs[name] = m # delete the methods, so zope's InterfaceClass doesn't see them. # Particularly necessary for things defined with IConstraints. del attrs[name] return remote_name, remote_attrs
class SetConstraint(OpenerConstraint): """The object must be a Set of some sort, with a given maximum size. To accept sets of any size, use maxLength=None. All member objects must obey the given constraint. By default this will accept both mutable and immutable sets, if you want to require a particular type, set mutable= to either True or False. """ # TODO: if mutable!=None, we won't throw out the wrong set type soon # enough. We need to override checkOpenType to accomplish this. opentypes = [("set",), ("immutable-set",)] name = "SetConstraint" def __init__(self, constraint, maxLength=None, mutable=None): self.constraint = IConstraint(constraint) self.maxLength = maxLength self.mutable = mutable def checkObject(self, obj, inbound): if not isinstance(obj, (set, frozenset)): raise Violation("not a set") if (self.mutable == True and not isinstance(obj, set)): raise Violation("obj is a set, but not a mutable one") if (self.mutable == False and not isinstance(obj, frozenset)): raise Violation("obj is a set, but not an immutable one") if self.maxLength is not None and len(obj) > self.maxLength: raise Violation("set is too large") if self.constraint: for o in obj: self.constraint.checkObject(o, inbound)
class DictConstraint(OpenerConstraint): opentypes = [("dict",)] name = "DictConstraint" def __init__(self, keyConstraint, valueConstraint, maxKeys=None): self.keyConstraint = IConstraint(keyConstraint) self.valueConstraint = IConstraint(valueConstraint) self.maxKeys = maxKeys def checkObject(self, obj, inbound): if not isinstance(obj, dict): raise Violation, "'%s' (%s) is not a Dictionary" % (obj, type(obj)) if self.maxKeys != None and len(obj) > self.maxKeys: raise Violation, "Dict keys=%d > maxKeys=%d" % (len(obj), self.maxKeys) for key, value in obj.iteritems(): self.keyConstraint.checkObject(key, inbound) self.valueConstraint.checkObject(value, inbound)
def adapt_obj_to_iconstraint(iface, t): if iface is not IConstraint: return None assert not IConstraint.providedBy(t) # not sure about this c = constraintMap.get(t, None) if c: return c for (typ, constraintMaker) in constraintTypeMap: if isinstance(t, typ): c = constraintMaker(t) if c: return c # RIFoo means accept either a Referenceable that implements RIFoo, or a # RemoteReference that points to just such a Referenceable. This is # hooked in by remoteinterface.py, when it calls addToConstraintTypeMap # we are the only way to make constraints raise UnknownSchemaType("can't make constraint from '%s' (%s)" % (t, type(t)))
def __init__(self, *elemConstraints): self.constraints = [IConstraint(e) for e in elemConstraints]
class RemoteReference(RemoteReferenceOnly): def callRemote(self, _name, *args, **kwargs): # Note: for consistency, *all* failures are reported asynchronously. return defer.maybeDeferred(self._callRemote, _name, *args, **kwargs) def callRemoteOnly(self, _name, *args, **kwargs): # the remote end will not send us a response. The only error cases # are arguments that don't match the schema, or broken invariants. In # particular, DeadReferenceError will be silently consumed. d = defer.maybeDeferred(self._callRemote, _name, _callOnly=True, *args, **kwargs) del d return None def _callRemote(self, _name, *args, **kwargs): req = None broker = self.tracker.broker # remember that "none" is not a valid constraint, so we use it to # mean "not set by the caller", which means we fall back to whatever # the RemoteInterface says. Using None would mean an AnyConstraint, # which is not the same thing. methodConstraintOverride = kwargs.get("_methodConstraint", "none") resultConstraint = kwargs.get("_resultConstraint", "none") useSchema = kwargs.get("_useSchema", True) callOnly = kwargs.get("_callOnly", False) if "_methodConstraint" in kwargs: del kwargs["_methodConstraint"] if "_resultConstraint" in kwargs: del kwargs["_resultConstraint"] if "_useSchema" in kwargs: del kwargs["_useSchema"] if "_callOnly" in kwargs: del kwargs["_callOnly"] if callOnly: if broker.disconnected: # DeadReferenceError is silently consumed return reqID = 0 else: # newRequestID() could fail with a DeadReferenceError reqID = broker.newRequestID() # in this section, we validate the outbound arguments against our # notion of what the other end will accept (the RemoteInterface) # first, figure out which method they want to invoke (interfaceName, methodName, methodSchema) = self._getMethodInfo(_name) req = call.PendingRequest(reqID, self, interfaceName, methodName) # TODO: consider adding a stringified stack trace to that # PendingRequest creation, so that DeadReferenceError can emit even # more information about the call which failed # for debugging: these are put into the messages emitted when # logRemoteFailures is turned on req.interfaceName = interfaceName req.methodName = methodName if methodConstraintOverride != "none": methodSchema = methodConstraintOverride if useSchema and methodSchema: # check args against the arg constraint. This could fail if # any arguments are of the wrong type try: methodSchema.checkAllArgs(args, kwargs, False) except Violation, v: v.setLocation("%s.%s(%s)" % (interfaceName, methodName, v.getLocation())) raise # the Interface gets to constraint the return value too, so # make a note of it to use later req.setConstraint(methodSchema.getResponseConstraint()) # if the caller specified a _resultConstraint, that overrides # the schema's one if resultConstraint != "none": # overrides schema req.setConstraint(IConstraint(resultConstraint)) clid = self.tracker.clid slicer = call.CallSlicer(reqID, clid, methodName, args, kwargs) # up to this point, we are not committed to sending anything to the # far end. The various phases of commitment are: # 1: once we tell our broker about the PendingRequest, we must # promise to retire it eventually. Specifically, if we encounter an # error before we give responsibility to the connection, we must # retire it ourselves. # 2: once we start sending the CallSlicer to the other end (in # particular, once they receive the reqID), they might send us a # response, so we must be prepared to handle that. Giving the # PendingRequest to the broker arranges for this to happen. # So all failures which occur before these commitment events are # entirely local: stale broker, bad method name, bad arguments. If # anything raises an exception before this point, the PendingRequest # is abandoned, and our maybeDeferred wrapper returns a failing # Deferred. # commitment point 1. We assume that if this call raises an # exception, the broker will be sure to not track the dead # PendingRequest if not callOnly: broker.addRequest(req) # if callOnly, the PendingRequest will never know about the # broker, and will therefore never ask to be removed from it # TODO: there is a decidability problem here: if the reqID made # it through, the other end will send us an answer (possibly an # error if the remaining slices were aborted). If not, we will # not get an answer. To decide whether we should remove our # broker.waitingForAnswers[] entry, we need to know how far the # slicing process made it. try: # commitment point 2 d = broker.send(slicer) # d will fire when the last argument has been serialized. It will # errback if the arguments (or any of their children) could not # be serialized. We need to catch this case and errback the # caller. # if we got here, we have been able to start serializing the # arguments. If serialization fails, the PendingRequest needs to # be flunked (because we aren't guaranteed that the far end will # do it). d.addErrback(req.fail) except: req.fail(failure.Failure()) # the remote end could send back an error response for many reasons: # bad method name # bad argument types (violated their schema) # exception during method execution # method result violated the results schema # something else could occur to cause an errback: # connection lost before response completely received # exception during deserialization of the response # [but only if it occurs after the reqID is received] # method result violated our results schema # if none of those occurred, the callback will be run return req.deferred
def __init__(self, keyConstraint, valueConstraint, maxKeys=None): self.keyConstraint = IConstraint(keyConstraint) self.valueConstraint = IConstraint(valueConstraint) self.maxKeys = maxKeys
class RemoteMethodSchema(object): """ This is a constraint for a single remotely-invokable method. It gets to require, deny, or impose further constraints upon a set of named arguments. This constraint is created by using keyword arguments with the same names as the target method's arguments. Two special names are used: __ignoreUnknown__: if True, unexpected argument names are silently dropped. (note that this makes the schema unbounded) __acceptUnknown__: if True, unexpected argument names are always accepted without a constraint (which also makes this schema unbounded) The remotely-accesible object's .getMethodSchema() method may return one of these objects. """ taster = {} # this should not be used as a top-level constraint opentypes = [] # overkill ignoreUnknown = False acceptUnknown = False name = None # method name, set when the RemoteInterface is parsed interface = None # points to the RemoteInterface which defines the method # under development def __init__(self, method=None, _response=None, __options=[], **kwargs): if method: self.initFromMethod(method) return self.argumentNames = [] self.argConstraints = {} self.required = [] self.responseConstraint = None # __response in the argslist gets treated specially, I think it is # mangled into _RemoteMethodSchema__response or something. When I # change it to use _response instead, it works. if _response: self.responseConstraint = IConstraint(_response) self.options = {} # return, wait, reliable, etc if "__ignoreUnknown__" in kwargs: self.ignoreUnknown = kwargs["__ignoreUnknown__"] del kwargs["__ignoreUnknown__"] if "__acceptUnknown__" in kwargs: self.acceptUnknown = kwargs["__acceptUnknown__"] del kwargs["__acceptUnknown__"] for argname, constraint in list(kwargs.items()): self.argumentNames.append(argname) constraint = IConstraint(constraint) self.argConstraints[argname] = constraint if not isinstance(constraint, Optional): self.required.append(argname) def initFromMethod(self, method): # call this with the Interface's prototype method: the one that has # argument constraints expressed as default arguments, and which # does nothing but returns the appropriate return type names, _, _, typeList = inspect.getargspec(method) if names and names[0] == 'self': why = "RemoteInterface methods should not have 'self' in their argument list" raise InvalidRemoteInterface(why) if not names: typeList = [] # 'def foo(oops)' results in typeList==None if typeList is None or len(names) != len(typeList): # TODO: relax this, use schema=Any for the args that don't have # default values. This would make: # def foo(a, b=int): return None # equivalent to: # def foo(a=Any, b=int): return None why = "RemoteInterface methods must have default values for all their arguments" raise InvalidRemoteInterface(why) self.argumentNames = names self.argConstraints = {} self.required = [] for i in range(len(names)): argname = names[i] constraint = typeList[i] if not isinstance(constraint, Optional): self.required.append(argname) self.argConstraints[argname] = IConstraint(constraint) # call the method, its 'return' value is the return constraint self.responseConstraint = IConstraint(method()) self.options = {} # return, wait, reliable, etc def getPositionalArgConstraint(self, argnum): if argnum >= len(self.argumentNames): raise Violation("too many positional arguments: %d >= %d" % (argnum, len(self.argumentNames))) argname = self.argumentNames[argnum] c = self.argConstraints.get(argname) assert c if isinstance(c, Optional): c = c.constraint return (True, c) def getKeywordArgConstraint(self, argname, num_posargs=0, previous_kwargs=[]): previous_args = self.argumentNames[:num_posargs] for pkw in previous_kwargs: assert pkw not in previous_args previous_args.append(pkw) if argname in previous_args: raise Violation("got multiple values for keyword argument '%s'" % (argname, )) c = self.argConstraints.get(argname) if c: if isinstance(c, Optional): c = c.constraint return (True, c) # what do we do with unknown arguments? if self.ignoreUnknown: return (False, None) if self.acceptUnknown: return (True, None) raise Violation("unknown argument '%s'" % argname) def getResponseConstraint(self): return self.responseConstraint def checkAllArgs(self, args, kwargs, inbound): # first we map the positional arguments allargs = {} if len(args) > len(self.argumentNames): raise Violation("method takes %d positional arguments (%d given)" % (len(self.argumentNames), len(args))) for i, argvalue in enumerate(args): allargs[self.argumentNames[i]] = argvalue for argname, argvalue in list(kwargs.items()): if argname in allargs: raise Violation( "got multiple values for keyword argument '%s'" % (argname, )) allargs[argname] = argvalue for argname, argvalue in list(allargs.items()): accept, constraint = self.getKeywordArgConstraint(argname) if not accept: # this argument will be ignored by the far end. TODO: emit a # warning pass try: constraint.checkObject(argvalue, inbound) except Violation as v: v.setLocation("%s=" % argname) raise for argname in self.required: if argname not in allargs: raise Violation("missing required argument '%s'" % argname) def checkResults(self, results, inbound): if self.responseConstraint: # this might raise a Violation. The caller will annotate its # location appropriately: they have more information than we do. self.responseConstraint.checkObject(results, inbound)
def __init__(self, constraint, maxLength=None, mutable=None): self.constraint = IConstraint(constraint) self.maxLength = maxLength self.mutable = mutable
def __init__(self, *alternatives): self.alternatives = [IConstraint(a) for a in alternatives] self.alternatives = tuple(self.alternatives)
def __init__(self, constraint, maxLength=None, minLength=0): self.constraint = IConstraint(constraint) self.maxLength = maxLength self.minLength = minLength