1
2import six
3import types
4import inspect
5from zope.interface import interface, providedBy, implementer
6from foolscap.constraint import Constraint, OpenerConstraint, nothingTaster, \
7     IConstraint, IRemoteMethodConstraint, Optional, Any
8from foolscap.tokens import Violation, InvalidRemoteInterface
9from foolscap.schema import addToConstraintTypeMap
10from foolscap import ipb
11
12class RemoteInterfaceClass(interface.InterfaceClass):
13    """This metaclass lets RemoteInterfaces be a lot like Interfaces. The
14    methods are parsed differently (PB needs more information from them than
15    z.i extracts, and the methods can be specified with a RemoteMethodSchema
16    directly).
17
18    RemoteInterfaces can accept the following additional attribute::
19
20     __remote_name__: can be set to a string to specify the globally-unique
21                      name for this interface. This should be a URL in a
22                      namespace you administer. If not set, defaults to the
23                      short classname.
24
25    RIFoo.names() returns the list of remote method names.
26
27    RIFoo['bar'] is still used to get information about method 'bar', however
28    it returns a RemoteMethodSchema instead of a z.i Method instance.
29
30    """
31
32    def __init__(self, iname, bases=(), attrs=None):
33        if attrs is None:
34            interface.InterfaceClass.__init__(self, iname, bases, attrs)
35            return
36
37        # parse (and remove) the attributes that make this a RemoteInterface
38        try:
39            rname, remote_attrs = self._parseRemoteInterface(iname, attrs)
40        except:
41            raise
42
43        # now let the normal InterfaceClass do its thing
44        interface.InterfaceClass.__init__(self, iname, bases, attrs)
45
46        # now add all the remote methods that InterfaceClass would have
47        # complained about. This is really gross, and it really makes me
48        # question why we're bothing to inherit from z.i.Interface at all. I
49        # will probably stop doing that soon, and just have our own
50        # meta-class, but I want to make sure you can still do
51        # 'implements(RIFoo)' from within a class definition.
52
53        a = getattr(self, "_InterfaceClass__attrs") # the ickiest part
54        a.update(remote_attrs)
55        self.__remote_name__ = rname
56
57        # finally, auto-register the interface
58        try:
59            registerRemoteInterface(self, rname)
60        except:
61            raise
62
63    def _parseRemoteInterface(self, iname, attrs):
64        remote_attrs = {}
65
66        remote_name = attrs.get("__remote_name__", iname)
67
68        # and see if there is a __remote_name__ . We delete it because
69        # InterfaceClass doesn't like arbitrary attributes
70        if "__remote_name__" in attrs:
71            del attrs["__remote_name__"]
72
73        # determine all remotely-callable methods
74        names = [name for name in list(attrs.keys())
75                 if ((type(attrs[name]) == types.FunctionType and
76                      not name.startswith("_")) or
77                     IConstraint.providedBy(attrs[name]))]
78
79        # turn them into constraints. Tag each of them with their name and
80        # the RemoteInterface they came from.
81        for name in names:
82            m = attrs[name]
83            if not IConstraint.providedBy(m):
84                m = RemoteMethodSchema(method=m)
85            m.name = name
86            m.interface = self
87            remote_attrs[name] = m
88            # delete the methods, so zope's InterfaceClass doesn't see them.
89            # Particularly necessary for things defined with IConstraints.
90            del attrs[name]
91
92        return remote_name, remote_attrs
93
94
95def getRemoteInterface(obj):
96    """Get the (one) RemoteInterface supported by the object, or None."""
97    interfaces = list(providedBy(obj))
98    # TODO: versioned Interfaces!
99    ilist = []
100    for i in interfaces:
101        if isinstance(i, RemoteInterfaceClass):
102            if i not in ilist:
103                ilist.append(i)
104    assert len(ilist) <= 1, ("don't use multiple RemoteInterfaces! %s uses %s"
105                             % (obj, ilist))
106    if ilist:
107        return ilist[0]
108    return None
109
110class DuplicateRemoteInterfaceError(Exception):
111    pass
112
113RemoteInterfaceRegistry = {}
114def registerRemoteInterface(iface, name=None):
115    if not name:
116        name = iface.__remote_name__
117    assert isinstance(iface, RemoteInterfaceClass)
118    if name in RemoteInterfaceRegistry:
119        old = RemoteInterfaceRegistry[name]
120        msg = "remote interface %s was registered with the same name (%s) as %s, please use __remote_name__ to provide a unique name" % (old, name, iface)
121        raise DuplicateRemoteInterfaceError(msg)
122    RemoteInterfaceRegistry[name] = iface
123
124def getRemoteInterfaceByName(iname):
125    return RemoteInterfaceRegistry.get(iname)
126
127
128@implementer(IRemoteMethodConstraint)
129class RemoteMethodSchema(object):
130    """
131    This is a constraint for a single remotely-invokable method. It gets to
132    require, deny, or impose further constraints upon a set of named
133    arguments.
134
135    This constraint is created by using keyword arguments with the same
136    names as the target method's arguments. Two special names are used:
137
138    __ignoreUnknown__: if True, unexpected argument names are silently
139    dropped. (note that this makes the schema unbounded)
140
141    __acceptUnknown__: if True, unexpected argument names are always
142    accepted without a constraint (which also makes this schema unbounded)
143
144    The remotely-accesible object's .getMethodSchema() method may return one
145    of these objects.
146    """
147
148    taster = {} # this should not be used as a top-level constraint
149    opentypes = [] # overkill
150    ignoreUnknown = False
151    acceptUnknown = False
152
153    name = None # method name, set when the RemoteInterface is parsed
154    interface = None # points to the RemoteInterface which defines the method
155
156    # under development
157    def __init__(self, method=None, _response=None, __options=[], **kwargs):
158        if method:
159            self.initFromMethod(method)
160            return
161        self.argumentNames = []
162        self.argConstraints = {}
163        self.required = []
164        self.responseConstraint = None
165        # __response in the argslist gets treated specially, I think it is
166        # mangled into _RemoteMethodSchema__response or something. When I
167        # change it to use _response instead, it works.
168        if _response:
169            self.responseConstraint = IConstraint(_response)
170        self.options = {} # return, wait, reliable, etc
171
172        if "__ignoreUnknown__" in kwargs:
173            self.ignoreUnknown = kwargs["__ignoreUnknown__"]
174            del kwargs["__ignoreUnknown__"]
175        if "__acceptUnknown__" in kwargs:
176            self.acceptUnknown = kwargs["__acceptUnknown__"]
177            del kwargs["__acceptUnknown__"]
178
179        for argname, constraint in list(kwargs.items()):
180            self.argumentNames.append(argname)
181            constraint = IConstraint(constraint)
182            self.argConstraints[argname] = constraint
183            if not isinstance(constraint, Optional):
184                self.required.append(argname)
185
186    def initFromMethod(self, method):
187        # call this with the Interface's prototype method: the one that has
188        # argument constraints expressed as default arguments, and which
189        # does nothing but returns the appropriate return type
190
191        names, _, _, typeList = inspect.getargspec(method)
192        if names and names[0] == 'self':
193            why = "RemoteInterface methods should not have 'self' in their argument list"
194            raise InvalidRemoteInterface(why)
195        if not names:
196            typeList = []
197        # 'def foo(oops)' results in typeList==None
198        if typeList is None or len(names) != len(typeList):
199            # TODO: relax this, use schema=Any for the args that don't have
200            # default values. This would make:
201            #  def foo(a, b=int): return None
202            # equivalent to:
203            #  def foo(a=Any, b=int): return None
204            why = "RemoteInterface methods must have default values for all their arguments"
205            raise InvalidRemoteInterface(why)
206        self.argumentNames = names
207        self.argConstraints = {}
208        self.required = []
209        for i in range(len(names)):
210            argname = names[i]
211            constraint = typeList[i]
212            if not isinstance(constraint, Optional):
213                self.required.append(argname)
214            self.argConstraints[argname] = IConstraint(constraint)
215
216        # call the method, its 'return' value is the return constraint
217        self.responseConstraint = IConstraint(method())
218        self.options = {} # return, wait, reliable, etc
219
220
221    def getPositionalArgConstraint(self, argnum):
222        if argnum >= len(self.argumentNames):
223            raise Violation("too many positional arguments: %d >= %d" %
224                            (argnum, len(self.argumentNames)))
225        argname = self.argumentNames[argnum]
226        c = self.argConstraints.get(argname)
227        assert c
228        if isinstance(c, Optional):
229            c = c.constraint
230        return (True, c)
231
232    def getKeywordArgConstraint(self, argname,
233                                num_posargs=0, previous_kwargs=[]):
234        previous_args = self.argumentNames[:num_posargs]
235        for pkw in previous_kwargs:
236            assert pkw not in previous_args
237            previous_args.append(pkw)
238        if argname in previous_args:
239            raise Violation("got multiple values for keyword argument '%s'"
240                            % (argname,))
241        c = self.argConstraints.get(argname)
242        if c:
243            if isinstance(c, Optional):
244                c = c.constraint
245            return (True, c)
246        # what do we do with unknown arguments?
247        if self.ignoreUnknown:
248            return (False, None)
249        if self.acceptUnknown:
250            return (True, None)
251        raise Violation("unknown argument '%s'" % argname)
252
253    def getResponseConstraint(self):
254        return self.responseConstraint
255
256    def checkAllArgs(self, args, kwargs, inbound):
257        # first we map the positional arguments
258        allargs = {}
259        if len(args) > len(self.argumentNames):
260            raise Violation("method takes %d positional arguments (%d given)"
261                            % (len(self.argumentNames), len(args)))
262        for i,argvalue in enumerate(args):
263            allargs[self.argumentNames[i]] = argvalue
264        for argname,argvalue in list(kwargs.items()):
265            if argname in allargs:
266                raise Violation("got multiple values for keyword argument '%s'"
267                                % (argname,))
268            allargs[argname] = argvalue
269
270        for argname, argvalue in list(allargs.items()):
271            accept, constraint = self.getKeywordArgConstraint(argname)
272            if not accept:
273                # this argument will be ignored by the far end. TODO: emit a
274                # warning
275                pass
276            try:
277                constraint.checkObject(argvalue, inbound)
278            except Violation as v:
279                v.setLocation("%s=" % argname)
280                raise
281
282        for argname in self.required:
283            if argname not in allargs:
284                raise Violation("missing required argument '%s'" % argname)
285
286    def checkResults(self, results, inbound):
287        if self.responseConstraint:
288            # this might raise a Violation. The caller will annotate its
289            # location appropriately: they have more information than we do.
290            self.responseConstraint.checkObject(results, inbound)
291
292@implementer(IRemoteMethodConstraint)
293class UnconstrainedMethod(object):
294    """I am a method constraint that accepts any arguments and any return
295    value.
296
297    To use this, assign it to a method name in a RemoteInterface::
298
299     class RIFoo(RemoteInterface):
300         def constrained_method(foo=int, bar=str): # this one is constrained
301             return str
302         not_method = UnconstrainedMethod()  # this one is not
303    """
304
305    def getPositionalArgConstraint(self, argnum):
306        return (True, Any())
307    def getKeywordArgConstraint(self, argname, num_posargs=0,
308                                previous_kwargs=[]):
309        return (True, Any())
310    def checkAllArgs(self, args, kwargs, inbound):
311        pass # accept everything
312    def getResponseConstraint(self):
313        return Any()
314    def checkResults(self, results, inbound):
315        pass # accept everything
316
317
318class LocalInterfaceConstraint(Constraint):
319    """This constraint accepts any (local) instance which implements the
320    given local Interface.
321    """
322
323    # TODO: maybe accept RemoteCopy instances
324    # TODO: accept inbound your-references, if the local object they map to
325    #       implements the interface
326
327    # TODO: do we need an string-to-Interface map just like we have a
328    # classname-to-class/factory map?
329    taster = nothingTaster
330    opentypes = []
331    name = "LocalInterfaceConstraint"
332
333    def __init__(self, interface):
334        self.interface = interface
335    def checkObject(self, obj, inbound):
336        # TODO: maybe try to get an adapter instead?
337        if not self.interface.providedBy(obj):
338            raise Violation("'%s' does not provide interface %s"
339                            % (obj, self.interface))
340
341class RemoteInterfaceConstraint(OpenerConstraint):
342    """This constraint accepts any RemoteReference that claims to be
343    associated with a remote Referenceable that implements the given
344    RemoteInterface. If 'interface' is None, just assert that it is a
345    RemoteReference at all.
346
347    On the inbound side, this will only accept a suitably-implementing
348    RemoteReference, or a gift that resolves to such a RemoteReference. On
349    the outbound side, this will accept either a Referenceable or a
350    RemoteReference (which might be a your-reference or a their-reference).
351
352    Sending your-references will result in the recipient getting a local
353    Referenceable, which will not pass the constraint. TODO: think about if
354    we want this behavior or not.
355    """
356
357    opentypes = [("my-reference",), ("their-reference",)]
358    name = "RemoteInterfaceConstraint"
359
360    def __init__(self, interface):
361        self.interface = interface
362    def checkObject(self, obj, inbound):
363        if inbound:
364            # this ought to be a RemoteReference that claims to be associated
365            # with a remote Referenceable that implements the desired
366            # interface.
367            if not ipb.IRemoteReference.providedBy(obj):
368                raise Violation("'%s' does not provide RemoteInterface %s, "
369                                "and doesn't even look like a RemoteReference"
370                                % (obj, self.interface))
371            if not self.interface:
372                return
373            iface = obj.tracker.interface
374            # TODO: this test probably doesn't handle subclasses of
375            # RemoteInterface, which might be useful (if it even works)
376            if not iface or iface != self.interface:
377                raise Violation("'%s' does not provide RemoteInterface %s"
378                                % (obj, self.interface))
379        else:
380            # this ought to be a Referenceable which implements the desired
381            # interface. Or, it might be a RemoteReference which points to
382            # one.
383            if ipb.IRemoteReference.providedBy(obj):
384                # it's a RemoteReference
385                if not self.interface:
386                    return
387                iface = obj.tracker.interface
388                if not iface or iface != self.interface:
389                    raise Violation("'%s' does not provide RemoteInterface %s"
390                                    % (obj, self.interface))
391                return
392            if not ipb.IReferenceable.providedBy(obj):
393                # TODO: maybe distinguish between OnlyReferenceable and
394                # Referenceable? which is more useful here?
395                raise Violation("'%s' is not a Referenceable" % (obj,))
396            if self.interface and not self.interface.providedBy(obj):
397                raise Violation("'%s' does not provide RemoteInterface %s"
398                                % (obj, self.interface))
399
400def _makeConstraint(t):
401    # This will be called for both local interfaces (IFoo) and remote
402    # interfaces (RIFoo), so we have to distinguish between them. The late
403    # import is to deal with a circular reference between this module and
404    # remoteinterface.py
405    if isinstance(t, RemoteInterfaceClass):
406        return RemoteInterfaceConstraint(t)
407    return LocalInterfaceConstraint(t)
408
409addToConstraintTypeMap(interface.InterfaceClass, _makeConstraint)
410
411
412# See
413# https://github.com/warner/foolscap/pull/76/commits/ff3b9e8c1e4fa13701273a2143ba80b1e58f47cf#r549428977
414# for more background on the use of add_metaclass here.
415class RemoteInterface(six.with_metaclass(RemoteInterfaceClass, interface.Interface)):
416    pass
417