rpc.py 19.7 KB
Newer Older
1 2 3 4 5 6 7
"""RPC Implemention, originally written for the Python Idle IDE

For security reasons, GvR requested that Idle's Python execution server process
connect to the Idle process, which listens for the connection.  Since Idle has
has only one client per server, this was not a limitation.

   +---------------------------------+ +-------------+
8
   | socketserver.BaseRequestHandler | | SocketIO    |
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
   +---------------------------------+ +-------------+
                   ^                   | register()  |
                   |                   | unregister()|
                   |                   +-------------+
                   |                      ^  ^
                   |                      |  |
                   | + -------------------+  |
                   | |                       |
   +-------------------------+        +-----------------+
   | RPCHandler              |        | RPCClient       |
   | [attribute of RPCServer]|        |                 |
   +-------------------------+        +-----------------+

The RPCServer handler class is expected to provide register/unregister methods.
RPCHandler inherits the mix-in class SocketIO, which provides these methods.

See the Idle run.main() docstring for further information on how this was
accomplished in Idle.

"""

import sys
31
import os
Chui Tey's avatar
Chui Tey committed
32 33
import socket
import select
34
import socketserver
Chui Tey's avatar
Chui Tey committed
35
import struct
36
import pickle
Chui Tey's avatar
Chui Tey committed
37
import threading
38
import queue
Chui Tey's avatar
Chui Tey committed
39
import traceback
40
import copyreg
Chui Tey's avatar
Chui Tey committed
41 42 43
import types
import marshal

44

Chui Tey's avatar
Chui Tey committed
45 46 47 48 49 50 51 52 53 54
def unpickle_code(ms):
    co = marshal.loads(ms)
    assert isinstance(co, types.CodeType)
    return co

def pickle_code(co):
    assert isinstance(co, types.CodeType)
    ms = marshal.dumps(co)
    return unpickle_code, (ms,)

55 56 57
# XXX KBK 24Aug02 function pickling capability not used in Idle
#  def unpickle_function(ms):
#      return ms
Chui Tey's avatar
Chui Tey committed
58

59 60
#  def pickle_function(fn):
#      assert isinstance(fn, type.FunctionType)
61
#      return repr(fn)
62

63 64
copyreg.pickle(types.CodeType, pickle_code, unpickle_code)
# copyreg.pickle(types.FunctionType, pickle_function, unpickle_function)
Chui Tey's avatar
Chui Tey committed
65 66

BUFSIZE = 8*1024
67
LOCALHOST = '127.0.0.1'
Chui Tey's avatar
Chui Tey committed
68

69
class RPCServer(socketserver.TCPServer):
Chui Tey's avatar
Chui Tey committed
70 71 72 73

    def __init__(self, addr, handlerclass=None):
        if handlerclass is None:
            handlerclass = RPCHandler
74
        socketserver.TCPServer.__init__(self, addr, handlerclass)
Chui Tey's avatar
Chui Tey committed
75

76 77 78 79 80 81
    def server_bind(self):
        "Override TCPServer method, no bind() phase for connecting entity"
        pass

    def server_activate(self):
        """Override TCPServer method, connect() instead of listen()
82

83 84 85 86 87
        Due to the reversed connection, self.server_address is actually the
        address of the Idle Client to which we are connecting.

        """
        self.socket.connect(self.server_address)
88

89 90 91
    def get_request(self):
        "Override TCPServer method, return already connected socket"
        return self.socket, self.server_address
Chui Tey's avatar
Chui Tey committed
92

Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
93
    def handle_error(self, request, client_address):
94 95 96 97 98 99 100
        """Override TCPServer method

        Error message goes to __stderr__.  No error message if exiting
        normally or socket raised EOF.  Other exceptions not handled in
        server code will cause os._exit.

        """
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
101 102 103 104
        try:
            raise
        except SystemExit:
            raise
105
        except:
106
            erf = sys.__stderr__
107 108
            print('\n' + '-'*40, file=erf)
            print('Unhandled server exception!', file=erf)
109
            print('Thread: %s' % threading.current_thread().name, file=erf)
110 111
            print('Client Address: ', client_address, file=erf)
            print('Request: ', repr(request), file=erf)
112
            traceback.print_exc(file=erf)
113 114
            print('\n*** Unrecoverable, server exiting!', file=erf)
            print('-'*40, file=erf)
115
            os._exit(0)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
116

117
#----------------- end class RPCServer --------------------
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
118

Chui Tey's avatar
Chui Tey committed
119
objecttable = {}
120 121
request_queue = queue.Queue(0)
response_queue = queue.Queue(0)
122

Chui Tey's avatar
Chui Tey committed
123

124
class SocketIO(object):
Chui Tey's avatar
Chui Tey committed
125

126 127
    nextseq = 0

Chui Tey's avatar
Chui Tey committed
128
    def __init__(self, sock, objtable=None, debugging=None):
129
        self.sockthread = threading.current_thread()
Chui Tey's avatar
Chui Tey committed
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
        if debugging is not None:
            self.debugging = debugging
        self.sock = sock
        if objtable is None:
            objtable = objecttable
        self.objtable = objtable
        self.responses = {}
        self.cvars = {}

    def close(self):
        sock = self.sock
        self.sock = None
        if sock is not None:
            sock.close()

145 146 147 148
    def exithook(self):
        "override for specific exit action"
        os._exit()

Chui Tey's avatar
Chui Tey committed
149 150 151
    def debug(self, *args):
        if not self.debugging:
            return
152
        s = self.location + " " + str(threading.current_thread().name)
Chui Tey's avatar
Chui Tey committed
153 154
        for a in args:
            s = s + " " + str(a)
155
        print(s, file=sys.__stderr__)
Chui Tey's avatar
Chui Tey committed
156 157 158 159 160 161 162 163 164 165

    def register(self, oid, object):
        self.objtable[oid] = object

    def unregister(self, oid):
        try:
            del self.objtable[oid]
        except KeyError:
            pass

166
    def localcall(self, seq, request):
167
        self.debug("localcall:", request)
Chui Tey's avatar
Chui Tey committed
168 169 170 171
        try:
            how, (oid, methodname, args, kwargs) = request
        except TypeError:
            return ("ERROR", "Bad request format")
172
        if oid not in self.objtable:
173
            return ("ERROR", "Unknown object id: %r" % (oid,))
Chui Tey's avatar
Chui Tey committed
174 175 176 177 178 179 180 181 182 183
        obj = self.objtable[oid]
        if methodname == "__methods__":
            methods = {}
            _getmethods(obj, methods)
            return ("OK", methods)
        if methodname == "__attributes__":
            attributes = {}
            _getattributes(obj, attributes)
            return ("OK", attributes)
        if not hasattr(obj, methodname):
184
            return ("ERROR", "Unsupported method name: %r" % (methodname,))
Chui Tey's avatar
Chui Tey committed
185 186
        method = getattr(obj, methodname)
        try:
187 188 189 190 191 192 193 194 195 196
            if how == 'CALL':
                ret = method(*args, **kwargs)
                if isinstance(ret, RemoteObject):
                    ret = remoteref(ret)
                return ("OK", ret)
            elif how == 'QUEUE':
                request_queue.put((seq, (method, args, kwargs)))
                return("QUEUED", None)
            else:
                return ("ERROR", "Unsupported message type: %s" % how)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
197 198
        except SystemExit:
            raise
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
199
        except socket.error:
200
            raise
Chui Tey's avatar
Chui Tey committed
201
        except:
202 203
            msg = "*** Internal Error: rpc.py:SocketIO.localcall()\n\n"\
                  " Object: %s \n Method: %s \n Args: %s\n"
204
            print(msg % (oid, method, args), file=sys.__stderr__)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
205
            traceback.print_exc(file=sys.__stderr__)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
206 207
            return ("EXCEPTION", None)

Chui Tey's avatar
Chui Tey committed
208
    def remotecall(self, oid, methodname, args, kwargs):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
209
        self.debug("remotecall:asynccall: ", oid, methodname)
Chui Tey's avatar
Chui Tey committed
210
        seq = self.asynccall(oid, methodname, args, kwargs)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
211
        return self.asyncreturn(seq)
Chui Tey's avatar
Chui Tey committed
212

213 214 215 216 217
    def remotequeue(self, oid, methodname, args, kwargs):
        self.debug("remotequeue:asyncqueue: ", oid, methodname)
        seq = self.asyncqueue(oid, methodname, args, kwargs)
        return self.asyncreturn(seq)

Chui Tey's avatar
Chui Tey committed
218
    def asynccall(self, oid, methodname, args, kwargs):
219
        request = ("CALL", (oid, methodname, args, kwargs))
220
        seq = self.newseq()
221
        if threading.current_thread() != self.sockthread:
222 223
            cvar = threading.Condition()
            self.cvars[seq] = cvar
224 225
        self.debug(("asynccall:%d:" % seq), oid, methodname, args, kwargs)
        self.putmessage((seq, request))
Chui Tey's avatar
Chui Tey committed
226 227
        return seq

228 229 230
    def asyncqueue(self, oid, methodname, args, kwargs):
        request = ("QUEUE", (oid, methodname, args, kwargs))
        seq = self.newseq()
231
        if threading.current_thread() != self.sockthread:
232 233 234 235 236 237
            cvar = threading.Condition()
            self.cvars[seq] = cvar
        self.debug(("asyncqueue:%d:" % seq), oid, methodname, args, kwargs)
        self.putmessage((seq, request))
        return seq

Chui Tey's avatar
Chui Tey committed
238
    def asyncreturn(self, seq):
239
        self.debug("asyncreturn:%d:call getresponse(): " % seq)
240
        response = self.getresponse(seq, wait=0.05)
241
        self.debug(("asyncreturn:%d:response: " % seq), response)
Chui Tey's avatar
Chui Tey committed
242 243 244 245 246 247
        return self.decoderesponse(response)

    def decoderesponse(self, response):
        how, what = response
        if how == "OK":
            return what
248 249
        if how == "QUEUED":
            return None
Chui Tey's avatar
Chui Tey committed
250
        if how == "EXCEPTION":
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
251 252
            self.debug("decoderesponse: EXCEPTION")
            return None
253 254 255 256
        if how == "EOF":
            self.debug("decoderesponse: EOF")
            self.decode_interrupthook()
            return None
Chui Tey's avatar
Chui Tey committed
257
        if how == "ERROR":
258
            self.debug("decoderesponse: Internal ERROR:", what)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
259 260
            raise RuntimeError(what)
        raise SystemError(how, what)
Chui Tey's avatar
Chui Tey committed
261

262 263 264 265
    def decode_interrupthook(self):
        ""
        raise EOFError

Chui Tey's avatar
Chui Tey committed
266
    def mainloop(self):
267 268
        """Listen on socket until I/O not ready or EOF

269
        pollresponse() will loop looking for seq number None, which
270
        never comes, and exit on EOFError.
271 272

        """
Chui Tey's avatar
Chui Tey committed
273
        try:
274
            self.getresponse(myseq=None, wait=0.05)
Chui Tey's avatar
Chui Tey committed
275
        except EOFError:
276 277
            self.debug("mainloop:return")
            return
Chui Tey's avatar
Chui Tey committed
278

279 280
    def getresponse(self, myseq, wait):
        response = self._getresponse(myseq, wait)
Chui Tey's avatar
Chui Tey committed
281 282 283 284 285 286 287 288 289
        if response is not None:
            how, what = response
            if how == "OK":
                response = how, self._proxify(what)
        return response

    def _proxify(self, obj):
        if isinstance(obj, RemoteProxy):
            return RPCProxy(self, obj.oid)
290
        if isinstance(obj, list):
291
            return list(map(self._proxify, obj))
Chui Tey's avatar
Chui Tey committed
292 293 294
        # XXX Check for other types -- not currently needed
        return obj

295
    def _getresponse(self, myseq, wait):
296
        self.debug("_getresponse:myseq:", myseq)
297
        if threading.current_thread() is self.sockthread:
298
            # this thread does all reading of requests or responses
Chui Tey's avatar
Chui Tey committed
299
            while 1:
300
                response = self.pollresponse(myseq, wait)
Chui Tey's avatar
Chui Tey committed
301 302 303
                if response is not None:
                    return response
        else:
304 305 306
            # wait for notification from socket handling thread
            cvar = self.cvars[myseq]
            cvar.acquire()
307
            while myseq not in self.responses:
308
                cvar.wait()
Chui Tey's avatar
Chui Tey committed
309
            response = self.responses[myseq]
310 311
            self.debug("_getresponse:%s: thread woke up: response: %s" %
                       (myseq, response))
Chui Tey's avatar
Chui Tey committed
312 313
            del self.responses[myseq]
            del self.cvars[myseq]
314
            cvar.release()
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
315
            return response
Chui Tey's avatar
Chui Tey committed
316 317 318 319 320 321

    def newseq(self):
        self.nextseq = seq = self.nextseq + 2
        return seq

    def putmessage(self, message):
322
        self.debug("putmessage:%d:" % message[0])
Chui Tey's avatar
Chui Tey committed
323 324
        try:
            s = pickle.dumps(message)
325
        except pickle.PicklingError:
326
            print("Cannot pickle:", repr(message), file=sys.__stderr__)
Chui Tey's avatar
Chui Tey committed
327 328 329
            raise
        s = struct.pack("<i", len(s)) + s
        while len(s) > 0:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
330
            try:
331 332
                r, w, x = select.select([], [self.sock], [])
                n = self.sock.send(s[:BUFSIZE])
333
            except (AttributeError, TypeError):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
334
                raise IOError("socket no longer exists")
335 336
            except socket.error:
                raise
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
337 338
            else:
                s = s[n:]
Chui Tey's avatar
Chui Tey committed
339

340
    buff = b''
Chui Tey's avatar
Chui Tey committed
341 342 343
    bufneed = 4
    bufstate = 0 # meaning: 0 => reading count; 1 => reading data

344
    def pollpacket(self, wait):
Chui Tey's avatar
Chui Tey committed
345
        self._stage0()
346
        if len(self.buff) < self.bufneed:
347 348
            r, w, x = select.select([self.sock.fileno()], [], [], wait)
            if len(r) == 0:
Chui Tey's avatar
Chui Tey committed
349 350 351 352 353 354 355
                return None
            try:
                s = self.sock.recv(BUFSIZE)
            except socket.error:
                raise EOFError
            if len(s) == 0:
                raise EOFError
356
            self.buff += s
Chui Tey's avatar
Chui Tey committed
357 358 359 360
            self._stage0()
        return self._stage1()

    def _stage0(self):
361 362 363
        if self.bufstate == 0 and len(self.buff) >= 4:
            s = self.buff[:4]
            self.buff = self.buff[4:]
Chui Tey's avatar
Chui Tey committed
364 365 366 367
            self.bufneed = struct.unpack("<i", s)[0]
            self.bufstate = 1

    def _stage1(self):
368 369 370
        if self.bufstate == 1 and len(self.buff) >= self.bufneed:
            packet = self.buff[:self.bufneed]
            self.buff = self.buff[self.bufneed:]
Chui Tey's avatar
Chui Tey committed
371 372 373 374
            self.bufneed = 4
            self.bufstate = 0
            return packet

375
    def pollmessage(self, wait):
Chui Tey's avatar
Chui Tey committed
376 377 378 379 380
        packet = self.pollpacket(wait)
        if packet is None:
            return None
        try:
            message = pickle.loads(packet)
381
        except pickle.UnpicklingError:
382 383
            print("-----------------------", file=sys.__stderr__)
            print("cannot unpickle packet:", repr(packet), file=sys.__stderr__)
Chui Tey's avatar
Chui Tey committed
384
            traceback.print_stack(file=sys.__stderr__)
385
            print("-----------------------", file=sys.__stderr__)
Chui Tey's avatar
Chui Tey committed
386 387 388
            raise
        return message

389
    def pollresponse(self, myseq, wait):
390 391
        """Handle messages received on the socket.

392 393 394 395 396
        Some messages received may be asynchronous 'call' or 'queue' requests,
        and some may be responses for other threads.

        'call' requests are passed to self.localcall() with the expectation of
        immediate execution, during which time the socket is not serviced.
397

398 399 400 401 402 403 404 405 406 407 408 409
        'queue' requests are used for tasks (which may block or hang) to be
        processed in a different thread.  These requests are fed into
        request_queue by self.localcall().  Responses to queued requests are
        taken from response_queue and sent across the link with the associated
        sequence numbers.  Messages in the queues are (sequence_number,
        request/response) tuples and code using this module removing messages
        from the request_queue is responsible for returning the correct
        sequence number in the response_queue.

        pollresponse() will loop until a response message with the myseq
        sequence number is received, and will save other responses in
        self.responses and notify the owning thread.
410 411

        """
Chui Tey's avatar
Chui Tey committed
412
        while 1:
413 414 415
            # send queued response if there is one available
            try:
                qmsg = response_queue.get(0)
416
            except queue.Empty:
417 418 419 420 421 422 423 424 425 426 427 428 429 430
                pass
            else:
                seq, response = qmsg
                message = (seq, ('OK', response))
                self.putmessage(message)
            # poll for message on link
            try:
                message = self.pollmessage(wait)
                if message is None:  # socket not ready
                    return None
            except EOFError:
                self.handle_EOF()
                return None
            except AttributeError:
Chui Tey's avatar
Chui Tey committed
431 432
                return None
            seq, resq = message
433
            how = resq[0]
434
            self.debug("pollresponse:%d:myseq:%s" % (seq, myseq))
435 436
            # process or queue a request
            if how in ("CALL", "QUEUE"):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
437
                self.debug("pollresponse:%d:localcall:call:" % seq)
438
                response = self.localcall(seq, resq)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
439 440
                self.debug("pollresponse:%d:localcall:response:%s"
                           % (seq, response))
441 442 443 444 445
                if how == "CALL":
                    self.putmessage((seq, response))
                elif how == "QUEUE":
                    # don't acknowledge the 'queue' request!
                    pass
Chui Tey's avatar
Chui Tey committed
446
                continue
447
            # return if completed message transaction
Chui Tey's avatar
Chui Tey committed
448 449
            elif seq == myseq:
                return resq
450
            # must be a response for a different thread:
Chui Tey's avatar
Chui Tey committed
451
            else:
452
                cv = self.cvars.get(seq, None)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
453
                # response involving unknown sequence number is discarded,
454
                # probably intended for prior incarnation of server
Chui Tey's avatar
Chui Tey committed
455
                if cv is not None:
456
                    cv.acquire()
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
457
                    self.responses[seq] = resq
Chui Tey's avatar
Chui Tey committed
458
                    cv.notify()
459
                    cv.release()
Chui Tey's avatar
Chui Tey committed
460
                continue
461

462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478
    def handle_EOF(self):
        "action taken upon link being closed by peer"
        self.EOFhook()
        self.debug("handle_EOF")
        for key in self.cvars:
            cv = self.cvars[key]
            cv.acquire()
            self.responses[key] = ('EOF', None)
            cv.notify()
            cv.release()
        # call our (possibly overridden) exit function
        self.exithook()

    def EOFhook(self):
        "Classes using rpc client/server can override to augment EOF action"
        pass

479
#----------------- end class SocketIO --------------------
Chui Tey's avatar
Chui Tey committed
480

481
class RemoteObject(object):
Chui Tey's avatar
Chui Tey committed
482 483 484 485 486 487 488 489
    # Token mix-in class
    pass

def remoteref(obj):
    oid = id(obj)
    objecttable[oid] = obj
    return RemoteProxy(oid)

490
class RemoteProxy(object):
Chui Tey's avatar
Chui Tey committed
491 492 493 494

    def __init__(self, oid):
        self.oid = oid

495
class RPCHandler(socketserver.BaseRequestHandler, SocketIO):
Chui Tey's avatar
Chui Tey committed
496

Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
497 498
    debugging = False
    location = "#S"  # Server
Chui Tey's avatar
Chui Tey committed
499 500 501 502

    def __init__(self, sock, addr, svr):
        svr.current_handler = self ## cgt xxx
        SocketIO.__init__(self, sock)
503
        socketserver.BaseRequestHandler.__init__(self, sock, addr, svr)
Chui Tey's avatar
Chui Tey committed
504 505

    def handle(self):
506
        "handle() method required by socketserver"
Chui Tey's avatar
Chui Tey committed
507 508 509 510 511 512 513
        self.mainloop()

    def get_remote_proxy(self, oid):
        return RPCProxy(self, oid)

class RPCClient(SocketIO):

Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
514 515 516
    debugging = False
    location = "#C"  # Client

517
    nextseq = 1 # Requests coming from the client are odd numbered
Chui Tey's avatar
Chui Tey committed
518 519

    def __init__(self, address, family=socket.AF_INET, type=socket.SOCK_STREAM):
520 521 522
        self.listening_sock = socket.socket(family, type)
        self.listening_sock.bind(address)
        self.listening_sock.listen(1)
523 524

    def accept(self):
525
        working_sock, address = self.listening_sock.accept()
526
        if self.debugging:
527
            print("****** Connection request from ", address, file=sys.__stderr__)
528
        if address[0] == LOCALHOST:
529
            SocketIO.__init__(self, working_sock)
530
        else:
531
            print("** Invalid host: ", address, file=sys.__stderr__)
532
            raise socket.error
Chui Tey's avatar
Chui Tey committed
533 534 535 536

    def get_remote_proxy(self, oid):
        return RPCProxy(self, oid)

537
class RPCProxy(object):
Chui Tey's avatar
Chui Tey committed
538 539 540 541 542 543 544 545 546 547 548 549 550 551 552

    __methods = None
    __attributes = None

    def __init__(self, sockio, oid):
        self.sockio = sockio
        self.oid = oid

    def __getattr__(self, name):
        if self.__methods is None:
            self.__getmethods()
        if self.__methods.get(name):
            return MethodProxy(self.sockio, self.oid, name)
        if self.__attributes is None:
            self.__getattributes()
553
        if name in self.__attributes:
554 555 556 557
            value = self.sockio.remotecall(self.oid, '__getattribute__',
                                           (name,), {})
            return value
        else:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
558
            raise AttributeError(name)
559

Chui Tey's avatar
Chui Tey committed
560 561 562 563 564 565 566 567 568 569 570 571 572
    def __getattributes(self):
        self.__attributes = self.sockio.remotecall(self.oid,
                                                "__attributes__", (), {})

    def __getmethods(self):
        self.__methods = self.sockio.remotecall(self.oid,
                                                "__methods__", (), {})

def _getmethods(obj, methods):
    # Helper to get a list of methods from an object
    # Adds names to dictionary argument 'methods'
    for name in dir(obj):
        attr = getattr(obj, name)
573
        if hasattr(attr, '__call__'):
Chui Tey's avatar
Chui Tey committed
574
            methods[name] = 1
575
    if isinstance(obj, type):
Chui Tey's avatar
Chui Tey committed
576 577 578 579 580 581
        for super in obj.__bases__:
            _getmethods(super, methods)

def _getattributes(obj, attributes):
    for name in dir(obj):
        attr = getattr(obj, name)
582
        if not hasattr(attr, '__call__'):
583
            attributes[name] = 1
Chui Tey's avatar
Chui Tey committed
584

585
class MethodProxy(object):
Chui Tey's avatar
Chui Tey committed
586 587 588 589 590 591 592 593 594 595 596

    def __init__(self, sockio, oid, name):
        self.sockio = sockio
        self.oid = oid
        self.name = name

    def __call__(self, *args, **kwargs):
        value = self.sockio.remotecall(self.oid, self.name, args, kwargs)
        return value


Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
597
# XXX KBK 09Sep03  We need a proper unit test for this module.  Previously
Benjamin Peterson's avatar
Benjamin Peterson committed
598
#                  existing test code was removed at Rev 1.27 (r34098).