Added custom multiprocessing module:
- allows starting new processes and controlling them remotely from the parent process - remote processes can run their own GUI, Qt signals can be connected between processes (in general this is not possible with the built-in multiprocessing module due to the use of fork() ). - Control works by a system of proxy-objects such that controlling a remote process looks almost exactly like working with local objects. - Uses sockets to communicate between processes (so in theory could be made to work over a network), but also includes a mode that uses fork() to allow fast parallelization. - Wicked-easy inline parallelization by adding only one line of code to break up work between processes (requires fork; sorry windows users)
This commit is contained in:
parent
c7a78642fd
commit
72006fe05b
84
examples/multiprocess.py
Normal file
84
examples/multiprocess.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import initExample ## Add path to library (just for examples; you do not need this)
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pyqtgraph.multiprocess as mp
|
||||||
|
from pyqtgraph.multiprocess.parallelizer import Parallelize #, Parallelizer
|
||||||
|
import time
|
||||||
|
|
||||||
|
print "\n=================\nParallelize"
|
||||||
|
tasks = [1,2,4,8]
|
||||||
|
results = [None] * len(tasks)
|
||||||
|
size = 2000000
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
with Parallelize(enumerate(tasks), results=results, workers=1) as tasker:
|
||||||
|
for i, x in tasker:
|
||||||
|
print i, x
|
||||||
|
tot = 0
|
||||||
|
for j in xrange(size):
|
||||||
|
tot += j * x
|
||||||
|
results[i] = tot
|
||||||
|
print results
|
||||||
|
print "serial:", time.time() - start
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
with Parallelize(enumerate(tasks), results=results) as tasker:
|
||||||
|
for i, x in tasker:
|
||||||
|
print i, x
|
||||||
|
tot = 0
|
||||||
|
for j in xrange(size):
|
||||||
|
tot += j * x
|
||||||
|
results[i] = tot
|
||||||
|
print results
|
||||||
|
print "parallel:", time.time() - start
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
print "\n=================\nStart Process"
|
||||||
|
proc = mp.Process()
|
||||||
|
import os
|
||||||
|
print "parent:", os.getpid(), "child:", proc.proc.pid
|
||||||
|
print "started"
|
||||||
|
rnp = proc._import('numpy')
|
||||||
|
arr = rnp.array([1,2,3,4])
|
||||||
|
print repr(arr)
|
||||||
|
print str(arr)
|
||||||
|
print "return value:", repr(arr.mean(_returnType='value'))
|
||||||
|
print "return proxy:", repr(arr.mean(_returnType='proxy'))
|
||||||
|
print "return auto: ", repr(arr.mean(_returnType='auto'))
|
||||||
|
proc.join()
|
||||||
|
print "process finished"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
print "\n=================\nStart ForkedProcess"
|
||||||
|
proc = mp.ForkedProcess()
|
||||||
|
rnp = proc._import('numpy')
|
||||||
|
arr = rnp.array([1,2,3,4])
|
||||||
|
print repr(arr)
|
||||||
|
print str(arr)
|
||||||
|
print repr(arr.mean())
|
||||||
|
proc.join()
|
||||||
|
print "process finished"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import pyqtgraph as pg
|
||||||
|
from pyqtgraph.Qt import QtCore, QtGui
|
||||||
|
app = pg.QtGui.QApplication([])
|
||||||
|
|
||||||
|
print "\n=================\nStart QtProcess"
|
||||||
|
proc = mp.QtProcess()
|
||||||
|
d1 = proc.transfer(np.random.normal(size=1000))
|
||||||
|
d2 = proc.transfer(np.random.normal(size=1000))
|
||||||
|
rpg = proc._import('pyqtgraph')
|
||||||
|
plt = rpg.plot(d1+d2)
|
||||||
|
|
||||||
|
|
||||||
|
## Start Qt event loop unless running in interactive mode or using pyside.
|
||||||
|
#import sys
|
||||||
|
#if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
|
||||||
|
#QtGui.QApplication.instance().exec_()
|
22
multiprocess/__init__.py
Normal file
22
multiprocess/__init__.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
"""
|
||||||
|
Multiprocessing utility library
|
||||||
|
(parallelization done the way I like it)
|
||||||
|
|
||||||
|
Luke Campagnola
|
||||||
|
2012.06.10
|
||||||
|
|
||||||
|
This library provides:
|
||||||
|
|
||||||
|
- simple mechanism for starting a new python interpreter process that can be controlled from the original process
|
||||||
|
(this allows, for example, displaying and manipulating plots in a remote process
|
||||||
|
while the parent process is free to do other work)
|
||||||
|
- proxy system that allows objects hosted in the remote process to be used as if they were local
|
||||||
|
- Qt signal connection between processes
|
||||||
|
- very simple in-line parallelization (fork only; does not work on windows) for number-crunching
|
||||||
|
|
||||||
|
TODO:
|
||||||
|
allow remote processes to serve as rendering engines that pass pixmaps back to the parent process for display
|
||||||
|
(RemoteGraphicsView class)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from processes import *
|
176
multiprocess/parallelizer.py
Normal file
176
multiprocess/parallelizer.py
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import os, sys, time, multiprocessing
|
||||||
|
from processes import ForkedProcess
|
||||||
|
from remoteproxy import ExitError
|
||||||
|
|
||||||
|
class Parallelize:
|
||||||
|
"""
|
||||||
|
Class for ultra-simple inline parallelization on multi-core CPUs
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
## Here is the serial (single-process) task:
|
||||||
|
|
||||||
|
tasks = [1, 2, 4, 8]
|
||||||
|
results = []
|
||||||
|
for task in tasks:
|
||||||
|
result = processTask(task)
|
||||||
|
results.append(result)
|
||||||
|
print results
|
||||||
|
|
||||||
|
|
||||||
|
## Here is the parallelized version:
|
||||||
|
|
||||||
|
tasks = [1, 2, 4, 8]
|
||||||
|
results = []
|
||||||
|
with Parallelize(tasks, workers=4, results=results) as tasker:
|
||||||
|
for task in tasker:
|
||||||
|
result = processTask(task)
|
||||||
|
tasker.results.append(result)
|
||||||
|
print results
|
||||||
|
|
||||||
|
|
||||||
|
The only major caveat is that *result* in the example above must be picklable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, tasks, workers=None, block=True, **kwds):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
tasks - list of objects to be processed (Parallelize will determine how to distribute the tasks)
|
||||||
|
workers - number of worker processes or None to use number of CPUs in the system
|
||||||
|
kwds - objects to be shared by proxy with child processes
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.block = block
|
||||||
|
if workers is None:
|
||||||
|
workers = multiprocessing.cpu_count()
|
||||||
|
if not hasattr(os, 'fork'):
|
||||||
|
workers = 1
|
||||||
|
self.workers = workers
|
||||||
|
self.tasks = list(tasks)
|
||||||
|
self.kwds = kwds
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.proc = None
|
||||||
|
workers = self.workers
|
||||||
|
if workers == 1:
|
||||||
|
return Tasker(None, self.tasks, self.kwds)
|
||||||
|
|
||||||
|
self.childs = []
|
||||||
|
|
||||||
|
## break up tasks into one set per worker
|
||||||
|
chunks = [[] for i in xrange(workers)]
|
||||||
|
i = 0
|
||||||
|
for i in range(len(self.tasks)):
|
||||||
|
chunks[i%workers].append(self.tasks[i])
|
||||||
|
|
||||||
|
## fork and assign tasks to each worker
|
||||||
|
for i in range(workers):
|
||||||
|
proc = ForkedProcess(target=None, preProxy=self.kwds)
|
||||||
|
if not proc.isParent:
|
||||||
|
self.proc = proc
|
||||||
|
return Tasker(proc, chunks[i], proc.forkedProxies)
|
||||||
|
else:
|
||||||
|
self.childs.append(proc)
|
||||||
|
|
||||||
|
## process events from workers until all have exited.
|
||||||
|
activeChilds = self.childs[:]
|
||||||
|
while len(activeChilds) > 0:
|
||||||
|
for ch in activeChilds:
|
||||||
|
rem = []
|
||||||
|
try:
|
||||||
|
ch.processRequests()
|
||||||
|
except ExitError:
|
||||||
|
rem.append(ch)
|
||||||
|
for ch in rem:
|
||||||
|
activeChilds.remove(ch)
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
return [] ## no tasks for parent process.
|
||||||
|
|
||||||
|
def __exit__(self, *exc_info):
|
||||||
|
if exc_info[0] is not None:
|
||||||
|
sys.excepthook(*exc_info)
|
||||||
|
if self.proc is not None:
|
||||||
|
os._exit(0)
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
## wait for all child processes to finish
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Tasker:
|
||||||
|
def __init__(self, proc, tasks, kwds):
|
||||||
|
self.proc = proc
|
||||||
|
self.tasks = tasks
|
||||||
|
for k, v in kwds.iteritems():
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
## we could fix this up such that tasks are retrieved from the parent process one at a time..
|
||||||
|
for task in self.tasks:
|
||||||
|
yield task
|
||||||
|
if self.proc is not None:
|
||||||
|
self.proc.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#class Parallelizer:
|
||||||
|
#"""
|
||||||
|
#Use::
|
||||||
|
|
||||||
|
#p = Parallelizer()
|
||||||
|
#with p(4) as i:
|
||||||
|
#p.finish(do_work(i))
|
||||||
|
#print p.results()
|
||||||
|
|
||||||
|
#"""
|
||||||
|
#def __init__(self):
|
||||||
|
#pass
|
||||||
|
|
||||||
|
#def __call__(self, n):
|
||||||
|
#self.replies = []
|
||||||
|
#self.conn = None ## indicates this is the parent process
|
||||||
|
#return Session(self, n)
|
||||||
|
|
||||||
|
#def finish(self, data):
|
||||||
|
#if self.conn is None:
|
||||||
|
#self.replies.append((self.i, data))
|
||||||
|
#else:
|
||||||
|
##print "send", self.i, data
|
||||||
|
#self.conn.send((self.i, data))
|
||||||
|
#os._exit(0)
|
||||||
|
|
||||||
|
#def result(self):
|
||||||
|
#print self.replies
|
||||||
|
|
||||||
|
#class Session:
|
||||||
|
#def __init__(self, par, n):
|
||||||
|
#self.par = par
|
||||||
|
#self.n = n
|
||||||
|
|
||||||
|
#def __enter__(self):
|
||||||
|
#self.childs = []
|
||||||
|
#for i in range(1, self.n):
|
||||||
|
#c1, c2 = multiprocessing.Pipe()
|
||||||
|
#pid = os.fork()
|
||||||
|
#if pid == 0: ## child
|
||||||
|
#self.par.i = i
|
||||||
|
#self.par.conn = c2
|
||||||
|
#self.childs = None
|
||||||
|
#c1.close()
|
||||||
|
#return i
|
||||||
|
#else:
|
||||||
|
#self.childs.append(c1)
|
||||||
|
#c2.close()
|
||||||
|
#self.par.i = 0
|
||||||
|
#return 0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#def __exit__(self, *exc_info):
|
||||||
|
#if exc_info[0] is not None:
|
||||||
|
#sys.excepthook(*exc_info)
|
||||||
|
#if self.childs is not None:
|
||||||
|
#self.par.replies.extend([conn.recv() for conn in self.childs])
|
||||||
|
#else:
|
||||||
|
#self.par.finish(None)
|
||||||
|
|
208
multiprocess/processes.py
Normal file
208
multiprocess/processes.py
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
from remoteproxy import RemoteEventHandler, ExitError, NoResultError, LocalObjectProxy, ObjectProxy
|
||||||
|
import subprocess, atexit, os, sys, time, random, socket
|
||||||
|
import cPickle as pickle
|
||||||
|
import multiprocessing.connection
|
||||||
|
|
||||||
|
class Process(RemoteEventHandler):
|
||||||
|
def __init__(self, name=None, target=None):
|
||||||
|
if target is None:
|
||||||
|
target = startEventLoop
|
||||||
|
if name is None:
|
||||||
|
name = str(self)
|
||||||
|
|
||||||
|
## random authentication key
|
||||||
|
authkey = ''.join([chr(random.getrandbits(7)) for i in range(20)])
|
||||||
|
|
||||||
|
## Listen for connection from remote process (and find free port number)
|
||||||
|
port = 10000
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
l = multiprocessing.connection.Listener(('localhost', int(port)), authkey=authkey)
|
||||||
|
break
|
||||||
|
except socket.error as ex:
|
||||||
|
if ex.errno != 98:
|
||||||
|
raise
|
||||||
|
port += 1
|
||||||
|
|
||||||
|
## start remote process, instruct it to run target function
|
||||||
|
self.proc = subprocess.Popen((sys.executable, __file__, 'remote'), stdin=subprocess.PIPE)
|
||||||
|
pickle.dump((name+'_child', port, authkey, target), self.proc.stdin)
|
||||||
|
self.proc.stdin.close()
|
||||||
|
|
||||||
|
## open connection for remote process
|
||||||
|
conn = l.accept()
|
||||||
|
RemoteEventHandler.__init__(self, conn, name+'_parent', pid=self.proc.pid)
|
||||||
|
|
||||||
|
atexit.register(self.join)
|
||||||
|
|
||||||
|
def join(self, timeout=10):
|
||||||
|
if self.proc.poll() is None:
|
||||||
|
self.close()
|
||||||
|
start = time.time()
|
||||||
|
while self.proc.poll() is None:
|
||||||
|
if timeout is not None and time.time() - start > timeout:
|
||||||
|
raise Exception('Timed out waiting for remote process to end.')
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
|
||||||
|
def startEventLoop(name, port, authkey):
|
||||||
|
conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey)
|
||||||
|
global HANDLER
|
||||||
|
HANDLER = RemoteEventHandler(conn, name, os.getppid())
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
HANDLER.processRequests() # exception raised when the loop should exit
|
||||||
|
time.sleep(0.01)
|
||||||
|
except ExitError:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
class ForkedProcess(RemoteEventHandler):
|
||||||
|
"""
|
||||||
|
ForkedProcess is a substitute for Process that uses os.fork() to generate a new process.
|
||||||
|
This is much faster than starting a completely new interpreter, but carries some caveats
|
||||||
|
and limitations:
|
||||||
|
- open file handles are shared with the parent process, which is potentially dangerous
|
||||||
|
- it is not possible to have a QApplication in both parent and child process
|
||||||
|
(unless both QApplications are created _after_ the call to fork())
|
||||||
|
- generally not thread-safe. Also, threads are not copied by fork(); the new process
|
||||||
|
will have only one thread that starts wherever fork() was called in the parent process.
|
||||||
|
- forked processes are unceremoniously terminated when join() is called; they are not
|
||||||
|
given any opportunity to clean up. (This prevents them calling any cleanup code that
|
||||||
|
was only intended to be used by the parent process)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name=None, target=0, preProxy=None):
|
||||||
|
"""
|
||||||
|
When initializing, an optional target may be given.
|
||||||
|
If no target is specified, self.eventLoop will be used.
|
||||||
|
If None is given, no target will be called (and it will be up
|
||||||
|
to the caller to properly shut down the forked process)
|
||||||
|
|
||||||
|
preProxy may be a dict of values that will appear as ObjectProxy
|
||||||
|
in the remote process (but do not need to be sent explicitly since
|
||||||
|
they are available immediately before the call to fork().
|
||||||
|
Proxies will be availabe as self.proxies[name].
|
||||||
|
"""
|
||||||
|
self.hasJoined = False
|
||||||
|
if target == 0:
|
||||||
|
target = self.eventLoop
|
||||||
|
if name is None:
|
||||||
|
name = str(self)
|
||||||
|
|
||||||
|
conn, remoteConn = multiprocessing.Pipe()
|
||||||
|
|
||||||
|
proxyIDs = {}
|
||||||
|
if preProxy is not None:
|
||||||
|
for k, v in preProxy.iteritems():
|
||||||
|
proxyId = LocalObjectProxy.registerObject(v)
|
||||||
|
proxyIDs[k] = proxyId
|
||||||
|
|
||||||
|
pid = os.fork()
|
||||||
|
if pid == 0:
|
||||||
|
self.isParent = False
|
||||||
|
conn.close()
|
||||||
|
sys.stdin.close() ## otherwise we screw with interactive prompts.
|
||||||
|
RemoteEventHandler.__init__(self, remoteConn, name+'_child', pid=os.getppid())
|
||||||
|
if target is not None:
|
||||||
|
target()
|
||||||
|
|
||||||
|
ppid = os.getppid()
|
||||||
|
self.forkedProxies = {}
|
||||||
|
for name, proxyId in proxyIDs.iteritems():
|
||||||
|
self.forkedProxies[name] = ObjectProxy(ppid, proxyId=proxyId, typeStr=repr(preProxy[name]))
|
||||||
|
else:
|
||||||
|
self.isParent = True
|
||||||
|
self.childPid = pid
|
||||||
|
remoteConn.close()
|
||||||
|
RemoteEventHandler.handlers = {} ## don't want to inherit any of this from the parent.
|
||||||
|
|
||||||
|
RemoteEventHandler.__init__(self, conn, name+'_parent', pid=pid)
|
||||||
|
atexit.register(self.join)
|
||||||
|
|
||||||
|
|
||||||
|
def eventLoop(self):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
self.processRequests() # exception raised when the loop should exit
|
||||||
|
time.sleep(0.01)
|
||||||
|
except ExitError:
|
||||||
|
sys.exit(0)
|
||||||
|
except:
|
||||||
|
print "Error occurred in forked event loop:"
|
||||||
|
sys.excepthook(*sys.exc_info())
|
||||||
|
|
||||||
|
def join(self, timeout=10):
|
||||||
|
if self.hasJoined:
|
||||||
|
return
|
||||||
|
#os.kill(pid, 9)
|
||||||
|
try:
|
||||||
|
self.close(callSync='sync', timeout=timeout, noCleanup=True) ## ask the child process to exit and require that it return a confirmation.
|
||||||
|
except IOError: ## probably remote process has already quit
|
||||||
|
pass
|
||||||
|
self.hasJoined = True
|
||||||
|
|
||||||
|
|
||||||
|
##Special set of subclasses that implement a Qt event loop instead.
|
||||||
|
|
||||||
|
class RemoteQtEventHandler(RemoteEventHandler):
|
||||||
|
def __init__(self, *args, **kwds):
|
||||||
|
RemoteEventHandler.__init__(self, *args, **kwds)
|
||||||
|
|
||||||
|
def startEventTimer(self):
|
||||||
|
from pyqtgraph.Qt import QtGui, QtCore
|
||||||
|
self.timer = QtCore.QTimer()
|
||||||
|
self.timer.timeout.connect(self.processRequests)
|
||||||
|
self.timer.start(10)
|
||||||
|
|
||||||
|
def processRequests(self):
|
||||||
|
try:
|
||||||
|
RemoteEventHandler.processRequests(self)
|
||||||
|
except ExitError:
|
||||||
|
from pyqtgraph.Qt import QtGui, QtCore
|
||||||
|
QtGui.QApplication.instance().quit()
|
||||||
|
self.timer.stop()
|
||||||
|
#raise
|
||||||
|
|
||||||
|
class QtProcess(Process):
|
||||||
|
def __init__(self, name=None):
|
||||||
|
Process.__init__(self, name, target=startQtEventLoop)
|
||||||
|
self.startEventTimer()
|
||||||
|
|
||||||
|
def startEventTimer(self):
|
||||||
|
from pyqtgraph.Qt import QtGui, QtCore ## avoid module-level import to keep bootstrap snappy.
|
||||||
|
self.timer = QtCore.QTimer()
|
||||||
|
app = QtGui.QApplication.instance()
|
||||||
|
if app is None:
|
||||||
|
raise Exception("Must create QApplication before starting QtProcess")
|
||||||
|
self.timer.timeout.connect(self.processRequests)
|
||||||
|
self.timer.start(10)
|
||||||
|
|
||||||
|
def processRequests(self):
|
||||||
|
try:
|
||||||
|
Process.processRequests(self)
|
||||||
|
except ExitError:
|
||||||
|
self.timer.stop()
|
||||||
|
|
||||||
|
def startQtEventLoop(name, port, authkey):
|
||||||
|
conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey)
|
||||||
|
from pyqtgraph.Qt import QtGui, QtCore
|
||||||
|
#from PyQt4 import QtGui, QtCore
|
||||||
|
app = QtGui.QApplication.instance()
|
||||||
|
#print app
|
||||||
|
if app is None:
|
||||||
|
app = QtGui.QApplication([])
|
||||||
|
app.setQuitOnLastWindowClosed(False) ## generally we want the event loop to stay open
|
||||||
|
## until it is explicitly closed by the parent process.
|
||||||
|
|
||||||
|
global HANDLER
|
||||||
|
HANDLER = RemoteQtEventHandler(conn, name, os.getppid())
|
||||||
|
HANDLER.startEventTimer()
|
||||||
|
app.exec_()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) == 2 and sys.argv[1] == 'remote': ## module has been invoked as script in new python interpreter.
|
||||||
|
name, port, authkey, target = pickle.load(sys.stdin)
|
||||||
|
target(name, port, authkey)
|
||||||
|
sys.exit(0)
|
759
multiprocess/remoteproxy.py
Normal file
759
multiprocess/remoteproxy.py
Normal file
@ -0,0 +1,759 @@
|
|||||||
|
import os, __builtin__, time, sys, traceback, weakref
|
||||||
|
import cPickle as pickle
|
||||||
|
|
||||||
|
class ExitError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NoResultError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteEventHandler(object):
|
||||||
|
|
||||||
|
handlers = {} ## maps {process ID : handler}. This allows unpickler to determine which process
|
||||||
|
## an object proxy belongs to
|
||||||
|
|
||||||
|
def __init__(self, connection, name, pid):
|
||||||
|
self.conn = connection
|
||||||
|
self.name = name
|
||||||
|
self.results = {} ## reqId: (status, result); cache of request results received from the remote process
|
||||||
|
## status is either 'result' or 'error'
|
||||||
|
## if 'error', then result will be (exception, formatted exceprion)
|
||||||
|
## where exception may be None if it could not be passed through the Connection.
|
||||||
|
|
||||||
|
self.proxies = {} ## maps {weakref(proxy): proxyId}; used to inform the remote process when a proxy has been deleted.
|
||||||
|
|
||||||
|
## attributes that affect the behavior of the proxy.
|
||||||
|
## See ObjectProxy._setProxyOptions for description
|
||||||
|
self.proxyOptions = {
|
||||||
|
'callSync': 'sync', ## 'sync', 'async', 'off'
|
||||||
|
'timeout': 10, ## float
|
||||||
|
'returnType': 'auto', ## 'proxy', 'value', 'auto'
|
||||||
|
'autoProxy': False, ## bool
|
||||||
|
'deferGetattr': False, ## True, False
|
||||||
|
'noProxyTypes': [ type(None), str, int, float, tuple, list, dict, LocalObjectProxy, ObjectProxy ],
|
||||||
|
}
|
||||||
|
|
||||||
|
self.nextRequestId = 0
|
||||||
|
self.exited = False
|
||||||
|
|
||||||
|
RemoteEventHandler.handlers[pid] = self ## register this handler as the one communicating with pid
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getHandler(cls, pid):
|
||||||
|
return cls.handlers[pid]
|
||||||
|
|
||||||
|
def getProxyOption(self, opt):
|
||||||
|
return self.proxyOptions[opt]
|
||||||
|
|
||||||
|
def setProxyOptions(self, **kwds):
|
||||||
|
"""
|
||||||
|
Set the default behavior options for object proxies.
|
||||||
|
See ObjectProxy._setProxyOptions for more info.
|
||||||
|
"""
|
||||||
|
self.proxyOptions.update(kwds)
|
||||||
|
|
||||||
|
def processRequests(self):
|
||||||
|
"""Process all pending requests from the pipe, return
|
||||||
|
after no more events are immediately available. (non-blocking)"""
|
||||||
|
if self.exited:
|
||||||
|
raise ExitError()
|
||||||
|
|
||||||
|
while self.conn.poll():
|
||||||
|
try:
|
||||||
|
self.handleRequest()
|
||||||
|
except ExitError:
|
||||||
|
self.exited = True
|
||||||
|
raise
|
||||||
|
except:
|
||||||
|
print "Error in process %s" % self.name
|
||||||
|
sys.excepthook(*sys.exc_info())
|
||||||
|
|
||||||
|
def handleRequest(self):
|
||||||
|
"""Handle a single request from the remote process.
|
||||||
|
Blocks until a request is available."""
|
||||||
|
result = None
|
||||||
|
try:
|
||||||
|
cmd, reqId, optStr = self.conn.recv() ## args, kwds are double-pickled to ensure this recv() call never fails
|
||||||
|
except EOFError:
|
||||||
|
## remote process has shut down; end event loop
|
||||||
|
raise ExitError()
|
||||||
|
except IOError:
|
||||||
|
raise ExitError()
|
||||||
|
#print os.getpid(), "received request:", cmd, reqId
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
if cmd == 'result' or cmd == 'error':
|
||||||
|
resultId = reqId
|
||||||
|
reqId = None ## prevents attempt to return information from this request
|
||||||
|
## (this is already a return from a previous request)
|
||||||
|
|
||||||
|
opts = pickle.loads(optStr)
|
||||||
|
#print os.getpid(), "received request:", cmd, reqId, opts
|
||||||
|
returnType = opts.get('returnType', 'auto')
|
||||||
|
|
||||||
|
if cmd == 'result':
|
||||||
|
self.results[resultId] = ('result', opts['result'])
|
||||||
|
elif cmd == 'error':
|
||||||
|
self.results[resultId] = ('error', (opts['exception'], opts['excString']))
|
||||||
|
elif cmd == 'getObjAttr':
|
||||||
|
result = getattr(opts['obj'], opts['attr'])
|
||||||
|
elif cmd == 'callObj':
|
||||||
|
obj = opts['obj']
|
||||||
|
fnargs = opts['args']
|
||||||
|
fnkwds = opts['kwds']
|
||||||
|
if len(fnkwds) == 0: ## need to do this because some functions do not allow keyword arguments.
|
||||||
|
#print obj, fnargs
|
||||||
|
result = obj(*fnargs)
|
||||||
|
else:
|
||||||
|
result = obj(*fnargs, **fnkwds)
|
||||||
|
elif cmd == 'getObjValue':
|
||||||
|
result = opts['obj'] ## has already been unpickled into its local value
|
||||||
|
returnType = 'value'
|
||||||
|
elif cmd == 'transfer':
|
||||||
|
result = opts['obj']
|
||||||
|
returnType = 'proxy'
|
||||||
|
elif cmd == 'import':
|
||||||
|
name = opts['module']
|
||||||
|
fromlist = opts.get('fromlist', [])
|
||||||
|
mod = __builtin__.__import__(name, fromlist=fromlist)
|
||||||
|
|
||||||
|
if len(fromlist) == 0:
|
||||||
|
parts = name.lstrip('.').split('.')
|
||||||
|
result = mod
|
||||||
|
for part in parts[1:]:
|
||||||
|
result = getattr(result, part)
|
||||||
|
else:
|
||||||
|
result = map(mod.__getattr__, fromlist)
|
||||||
|
|
||||||
|
elif cmd == 'del':
|
||||||
|
LocalObjectProxy.releaseProxyId(opts['proxyId'])
|
||||||
|
#del self.proxiedObjects[opts['objId']]
|
||||||
|
|
||||||
|
elif cmd == 'close':
|
||||||
|
if reqId is not None:
|
||||||
|
result = True
|
||||||
|
returnType = 'value'
|
||||||
|
|
||||||
|
exc = None
|
||||||
|
except:
|
||||||
|
exc = sys.exc_info()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if reqId is not None:
|
||||||
|
if exc is None:
|
||||||
|
#print "returnValue:", returnValue, result
|
||||||
|
if returnType == 'auto':
|
||||||
|
result = self.autoProxy(result, self.proxyOptions['noProxyTypes'])
|
||||||
|
elif returnType == 'proxy':
|
||||||
|
result = LocalObjectProxy(result)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.replyResult(reqId, result)
|
||||||
|
except:
|
||||||
|
sys.excepthook(*sys.exc_info())
|
||||||
|
self.replyError(reqId, *sys.exc_info())
|
||||||
|
else:
|
||||||
|
self.replyError(reqId, *exc)
|
||||||
|
|
||||||
|
elif exc is not None:
|
||||||
|
sys.excepthook(*exc)
|
||||||
|
|
||||||
|
if cmd == 'close':
|
||||||
|
if opts.get('noCleanup', False) is True:
|
||||||
|
os._exit(0) ## exit immediately, do not pass GO, do not collect $200.
|
||||||
|
## (more importantly, do not call any code that would
|
||||||
|
## normally be invoked at exit)
|
||||||
|
else:
|
||||||
|
raise ExitError()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def replyResult(self, reqId, result):
|
||||||
|
self.send(request='result', reqId=reqId, callSync='off', opts=dict(result=result))
|
||||||
|
|
||||||
|
def replyError(self, reqId, *exc):
|
||||||
|
excStr = traceback.format_exception(*exc)
|
||||||
|
try:
|
||||||
|
self.send(request='error', reqId=reqId, callSync='off', opts=dict(exception=exc[1], excString=excStr))
|
||||||
|
except:
|
||||||
|
self.send(request='error', reqId=reqId, callSync='off', opts=dict(exception=None, excString=excStr))
|
||||||
|
|
||||||
|
def send(self, request, opts=None, reqId=None, callSync='sync', timeout=10, returnType=None, **kwds):
|
||||||
|
"""Send a request or return packet to the remote process.
|
||||||
|
Generally it is not necessary to call this method directly; it is for internal use.
|
||||||
|
(The docstring has information that is nevertheless useful to the programmer
|
||||||
|
as it describes the internal protocol used to communicate between processes)
|
||||||
|
|
||||||
|
========== ====================================================================
|
||||||
|
Arguments:
|
||||||
|
request String describing the type of request being sent (see below)
|
||||||
|
reqId Integer uniquely linking a result back to the request that generated
|
||||||
|
it. (most requests leave this blank)
|
||||||
|
callSync 'sync': return the actual result of the request
|
||||||
|
'async': return a Request object which can be used to look up the
|
||||||
|
result later
|
||||||
|
'off': return no result
|
||||||
|
timeout Time in seconds to wait for a response when callSync=='sync'
|
||||||
|
opts Extra arguments sent to the remote process that determine the way
|
||||||
|
the request will be handled (see below)
|
||||||
|
returnType 'proxy', 'value', or 'auto'
|
||||||
|
========== ====================================================================
|
||||||
|
|
||||||
|
Description of request strings and options allowed for each:
|
||||||
|
|
||||||
|
============= ============= ========================================================
|
||||||
|
request option description
|
||||||
|
------------- ------------- --------------------------------------------------------
|
||||||
|
getObjAttr Request the remote process return (proxy to) an
|
||||||
|
attribute of an object.
|
||||||
|
obj reference to object whose attribute should be
|
||||||
|
returned
|
||||||
|
attr string name of attribute to return
|
||||||
|
returnValue bool or 'auto' indicating whether to return a proxy or
|
||||||
|
the actual value.
|
||||||
|
|
||||||
|
callObj Request the remote process call a function or
|
||||||
|
method. If a request ID is given, then the call's
|
||||||
|
return value will be sent back (or information
|
||||||
|
about the error that occurred while running the
|
||||||
|
function)
|
||||||
|
obj the (reference to) object to call
|
||||||
|
args tuple of arguments to pass to callable
|
||||||
|
kwds dict of keyword arguments to pass to callable
|
||||||
|
returnValue bool or 'auto' indicating whether to return a proxy or
|
||||||
|
the actual value.
|
||||||
|
|
||||||
|
getObjValue Request the remote process return the value of
|
||||||
|
a proxied object (must be picklable)
|
||||||
|
obj reference to object whose value should be returned
|
||||||
|
|
||||||
|
transfer Copy an object to the remote process and request
|
||||||
|
it return a proxy for the new object.
|
||||||
|
obj The object to transfer.
|
||||||
|
|
||||||
|
import Request the remote process import new symbols
|
||||||
|
and return proxy(ies) to the imported objects
|
||||||
|
module the string name of the module to import
|
||||||
|
fromlist optional list of string names to import from module
|
||||||
|
|
||||||
|
del Inform the remote process that a proxy has been
|
||||||
|
released (thus the remote process may be able to
|
||||||
|
release the original object)
|
||||||
|
proxyId id of proxy which is no longer referenced by
|
||||||
|
remote host
|
||||||
|
|
||||||
|
close Instruct the remote process to stop its event loop
|
||||||
|
and exit. Optionally, this request may return a
|
||||||
|
confirmation.
|
||||||
|
|
||||||
|
result Inform the remote process that its request has
|
||||||
|
been processed
|
||||||
|
result return value of a request
|
||||||
|
|
||||||
|
error Inform the remote process that its request failed
|
||||||
|
exception the Exception that was raised (or None if the
|
||||||
|
exception could not be pickled)
|
||||||
|
excString string-formatted version of the exception and
|
||||||
|
traceback
|
||||||
|
============= =====================================================================
|
||||||
|
"""
|
||||||
|
#if len(kwds) > 0:
|
||||||
|
#print "Warning: send() ignored args:", kwds
|
||||||
|
|
||||||
|
if opts is None:
|
||||||
|
opts = {}
|
||||||
|
|
||||||
|
assert callSync in ['off', 'sync', 'async'], 'callSync must be one of "off", "sync", or "async"'
|
||||||
|
if reqId is None:
|
||||||
|
if callSync != 'off': ## requested return value; use the next available request ID
|
||||||
|
reqId = self.nextRequestId
|
||||||
|
self.nextRequestId += 1
|
||||||
|
else:
|
||||||
|
## If requestId is provided, this _must_ be a response to a previously received request.
|
||||||
|
assert request in ['result', 'error']
|
||||||
|
|
||||||
|
if returnType is not None:
|
||||||
|
opts['returnType'] = returnType
|
||||||
|
#print "send", opts
|
||||||
|
## double-pickle args to ensure that at least status and request ID get through
|
||||||
|
try:
|
||||||
|
optStr = pickle.dumps(opts)
|
||||||
|
except:
|
||||||
|
print "Error pickling:", opts
|
||||||
|
raise
|
||||||
|
|
||||||
|
request = (request, reqId, optStr)
|
||||||
|
self.conn.send(request)
|
||||||
|
|
||||||
|
if callSync == 'off':
|
||||||
|
return
|
||||||
|
|
||||||
|
req = Request(self, reqId, description=str(request), timeout=timeout)
|
||||||
|
if callSync == 'async':
|
||||||
|
return req
|
||||||
|
|
||||||
|
if callSync == 'sync':
|
||||||
|
try:
|
||||||
|
return req.result()
|
||||||
|
except NoResultError:
|
||||||
|
return req
|
||||||
|
|
||||||
|
def close(self, callSync='off', noCleanup=False, **kwds):
|
||||||
|
self.send(request='close', opts=dict(noCleanup=noCleanup), callSync=callSync, **kwds)
|
||||||
|
|
||||||
|
def getResult(self, reqId):
|
||||||
|
## raises NoResultError if the result is not available yet
|
||||||
|
#print self.results.keys(), os.getpid()
|
||||||
|
if reqId not in self.results:
|
||||||
|
#self.readPipe()
|
||||||
|
try:
|
||||||
|
self.processRequests()
|
||||||
|
except ExitError:
|
||||||
|
pass
|
||||||
|
if reqId not in self.results:
|
||||||
|
raise NoResultError()
|
||||||
|
status, result = self.results.pop(reqId)
|
||||||
|
if status == 'result':
|
||||||
|
return result
|
||||||
|
elif status == 'error':
|
||||||
|
#print ''.join(result)
|
||||||
|
exc, excStr = result
|
||||||
|
if exc is not None:
|
||||||
|
print "===== Remote process raised exception on request: ====="
|
||||||
|
print ''.join(excStr)
|
||||||
|
print "===== Local Traceback to request follows: ====="
|
||||||
|
raise exc
|
||||||
|
else:
|
||||||
|
print ''.join(excStr)
|
||||||
|
raise Exception("Error getting result. See above for exception from remote process.")
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception("Internal error.")
|
||||||
|
|
||||||
|
def _import(self, mod, **kwds):
|
||||||
|
"""
|
||||||
|
Request the remote process import a module (or symbols from a module)
|
||||||
|
and return the proxied results. Uses built-in __import__() function, but
|
||||||
|
adds a bit more processing:
|
||||||
|
|
||||||
|
_import('module') => returns module
|
||||||
|
_import('module.submodule') => returns submodule
|
||||||
|
(note this differs from behavior of __import__)
|
||||||
|
_import('module', fromlist=[name1, name2, ...]) => returns [module.name1, module.name2, ...]
|
||||||
|
(this also differs from behavior of __import__)
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.send(request='import', callSync='sync', opts=dict(module=mod), **kwds)
|
||||||
|
|
||||||
|
def getObjAttr(self, obj, attr, **kwds):
|
||||||
|
return self.send(request='getObjAttr', opts=dict(obj=obj, attr=attr), **kwds)
|
||||||
|
|
||||||
|
def getObjValue(self, obj, **kwds):
|
||||||
|
return self.send(request='getObjValue', opts=dict(obj=obj), **kwds)
|
||||||
|
|
||||||
|
def callObj(self, obj, args, kwds, **opts):
|
||||||
|
opts = opts.copy()
|
||||||
|
noProxyTypes = opts.pop('noProxyTypes', None)
|
||||||
|
if noProxyTypes is None:
|
||||||
|
noProxyTypes = self.proxyOptions['noProxyTypes']
|
||||||
|
autoProxy = opts.pop('autoProxy', self.proxyOptions['autoProxy'])
|
||||||
|
|
||||||
|
if autoProxy is True:
|
||||||
|
args = tuple([self.autoProxy(v, noProxyTypes) for v in args])
|
||||||
|
for k, v in kwds.iteritems():
|
||||||
|
opts[k] = self.autoProxy(v, noProxyTypes)
|
||||||
|
|
||||||
|
return self.send(request='callObj', opts=dict(obj=obj, args=args, kwds=kwds), **opts)
|
||||||
|
|
||||||
|
def registerProxy(self, proxy):
|
||||||
|
ref = weakref.ref(proxy, self.deleteProxy)
|
||||||
|
self.proxies[ref] = proxy._proxyId
|
||||||
|
|
||||||
|
def deleteProxy(self, ref):
|
||||||
|
proxyId = self.proxies.pop(ref)
|
||||||
|
try:
|
||||||
|
self.send(request='del', opts=dict(proxyId=proxyId), callSync='off')
|
||||||
|
except IOError: ## if remote process has closed down, there is no need to send delete requests anymore
|
||||||
|
pass
|
||||||
|
|
||||||
|
def transfer(self, obj, **kwds):
|
||||||
|
"""
|
||||||
|
Transfer an object to the remote host (the object must be picklable) and return
|
||||||
|
a proxy for the new remote object.
|
||||||
|
"""
|
||||||
|
return self.send(request='transfer', opts=dict(obj=obj), **kwds)
|
||||||
|
|
||||||
|
def autoProxy(self, obj, noProxyTypes):
|
||||||
|
## Return object wrapped in LocalObjectProxy _unless_ its type is in noProxyTypes.
|
||||||
|
for typ in noProxyTypes:
|
||||||
|
if isinstance(obj, typ):
|
||||||
|
return obj
|
||||||
|
return LocalObjectProxy(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class Request:
|
||||||
|
## used internally for tracking asynchronous requests and returning results
|
||||||
|
def __init__(self, process, reqId, description=None, timeout=10):
|
||||||
|
self.proc = process
|
||||||
|
self.description = description
|
||||||
|
self.reqId = reqId
|
||||||
|
self.gotResult = False
|
||||||
|
self._result = None
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
def result(self, block=True, timeout=None):
|
||||||
|
"""Return the result for this request.
|
||||||
|
If block is True, wait until the result has arrived or *timeout* seconds passes.
|
||||||
|
If the timeout is reached, raise an exception. (use timeout=None to disable)
|
||||||
|
If block is False, raises an exception if the result has not arrived yet."""
|
||||||
|
|
||||||
|
if self.gotResult:
|
||||||
|
return self._result
|
||||||
|
|
||||||
|
if timeout is None:
|
||||||
|
timeout = self.timeout
|
||||||
|
|
||||||
|
if block:
|
||||||
|
start = time.time()
|
||||||
|
while not self.hasResult():
|
||||||
|
time.sleep(0.005)
|
||||||
|
if timeout >= 0 and time.time() - start > timeout:
|
||||||
|
print "Request timed out:", self.description
|
||||||
|
import traceback
|
||||||
|
traceback.print_stack()
|
||||||
|
raise NoResultError()
|
||||||
|
return self._result
|
||||||
|
else:
|
||||||
|
self._result = self.proc.getResult(self.reqId) ## raises NoResultError if result is not available yet
|
||||||
|
self.gotResult = True
|
||||||
|
return self._result
|
||||||
|
|
||||||
|
def hasResult(self):
|
||||||
|
"""Returns True if the result for this request has arrived."""
|
||||||
|
try:
|
||||||
|
#print "check result", self.description
|
||||||
|
self.result(block=False)
|
||||||
|
except NoResultError:
|
||||||
|
#print " -> not yet"
|
||||||
|
pass
|
||||||
|
|
||||||
|
return self.gotResult
|
||||||
|
|
||||||
|
class LocalObjectProxy(object):
|
||||||
|
"""Used for wrapping local objects to ensure that they are send by proxy to a remote host."""
|
||||||
|
nextProxyId = 0
|
||||||
|
proxiedObjects = {} ## maps {proxyId: object}
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def registerObject(cls, obj):
|
||||||
|
## assign it a unique ID so we can keep a reference to the local object
|
||||||
|
|
||||||
|
pid = cls.nextProxyId
|
||||||
|
cls.nextProxyId += 1
|
||||||
|
cls.proxiedObjects[pid] = obj
|
||||||
|
#print "register:", cls.proxiedObjects
|
||||||
|
return pid
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def lookupProxyId(cls, pid):
|
||||||
|
return cls.proxiedObjects[pid]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def releaseProxyId(cls, pid):
|
||||||
|
del cls.proxiedObjects[pid]
|
||||||
|
#print "release:", cls.proxiedObjects
|
||||||
|
|
||||||
|
def __init__(self, obj):
|
||||||
|
self.processId = os.getpid()
|
||||||
|
#self.objectId = id(obj)
|
||||||
|
self.typeStr = repr(obj)
|
||||||
|
#self.handler = handler
|
||||||
|
self.obj = obj
|
||||||
|
|
||||||
|
def __reduce__(self):
|
||||||
|
## a proxy is being pickled and sent to a remote process.
|
||||||
|
## every time this happens, a new proxy will be generated in the remote process,
|
||||||
|
## so we keep a new ID so we can track when each is released.
|
||||||
|
pid = LocalObjectProxy.registerObject(self.obj)
|
||||||
|
return (unpickleObjectProxy, (self.processId, pid, self.typeStr))
|
||||||
|
|
||||||
|
## alias
|
||||||
|
proxy = LocalObjectProxy
|
||||||
|
|
||||||
|
def unpickleObjectProxy(processId, proxyId, typeStr, attributes=None):
|
||||||
|
if processId == os.getpid():
|
||||||
|
obj = LocalObjectProxy.lookupProxyId(proxyId)
|
||||||
|
if attributes is not None:
|
||||||
|
for attr in attributes:
|
||||||
|
obj = getattr(obj, attr)
|
||||||
|
return obj
|
||||||
|
else:
|
||||||
|
return ObjectProxy(processId, proxyId=proxyId, typeStr=typeStr)
|
||||||
|
|
||||||
|
class ObjectProxy(object):
|
||||||
|
"""
|
||||||
|
Proxy to an object stored by the remote process. Proxies are created
|
||||||
|
by calling Process._import(), Process.transfer(), or by requesting/calling
|
||||||
|
attributes on existing proxy objects.
|
||||||
|
|
||||||
|
For the most part, this object can be used exactly as if it
|
||||||
|
were a local object.
|
||||||
|
"""
|
||||||
|
def __init__(self, processId, proxyId, typeStr='', parent=None):
|
||||||
|
object.__init__(self)
|
||||||
|
## can't set attributes directly because setattr is overridden.
|
||||||
|
self.__dict__['_processId'] = processId
|
||||||
|
self.__dict__['_typeStr'] = typeStr
|
||||||
|
self.__dict__['_proxyId'] = proxyId
|
||||||
|
self.__dict__['_attributes'] = ()
|
||||||
|
## attributes that affect the behavior of the proxy.
|
||||||
|
## in all cases, a value of None causes the proxy to ask
|
||||||
|
## its parent event handler to make the decision
|
||||||
|
self.__dict__['_proxyOptions'] = {
|
||||||
|
'callSync': None, ## 'sync', 'async', None
|
||||||
|
'timeout': None, ## float, None
|
||||||
|
'returnType': None, ## 'proxy', 'value', 'auto', None
|
||||||
|
'deferGetattr': None, ## True, False, None
|
||||||
|
'noProxyTypes': None, ## list of types to send by value instead of by proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
self.__dict__['_handler'] = RemoteEventHandler.getHandler(processId)
|
||||||
|
self.__dict__['_handler'].registerProxy(self) ## handler will watch proxy; inform remote process when the proxy is deleted.
|
||||||
|
|
||||||
|
def _setProxyOptions(self, **kwds):
|
||||||
|
"""
|
||||||
|
Change the behavior of this proxy. For all options, a value of None
|
||||||
|
will cause the proxy to instead use the default behavior defined
|
||||||
|
by its parent Process.
|
||||||
|
|
||||||
|
Options are:
|
||||||
|
|
||||||
|
============= =============================================================
|
||||||
|
callSync 'sync', 'async', 'off', or None.
|
||||||
|
If 'async', then calling methods will return a Request object
|
||||||
|
which can be used to inquire later about the result of the
|
||||||
|
method call.
|
||||||
|
If 'sync', then calling a method
|
||||||
|
will block until the remote process has returned its result
|
||||||
|
or the timeout has elapsed (in this case, a Request object
|
||||||
|
is returned instead).
|
||||||
|
If 'off', then the remote process is instructed _not_ to
|
||||||
|
reply and the method call will return None immediately.
|
||||||
|
returnType 'auto', 'proxy', 'value', or None.
|
||||||
|
If 'proxy', then the value returned when calling a method
|
||||||
|
will be a proxy to the object on the remote process.
|
||||||
|
If 'value', then attempt to pickle the returned object and
|
||||||
|
send it back.
|
||||||
|
If 'auto', then the decision is made by consulting the
|
||||||
|
'noProxyTypes' option.
|
||||||
|
autoProxy bool or None. If True, arguments to __call__ are
|
||||||
|
automatically converted to proxy unless their type is
|
||||||
|
listed in noProxyTypes (see below). If False, arguments
|
||||||
|
are left untouched. Use proxy(obj) to manually convert
|
||||||
|
arguments before sending.
|
||||||
|
timeout float or None. Length of time to wait during synchronous
|
||||||
|
requests before returning a Request object instead.
|
||||||
|
deferGetattr True, False, or None.
|
||||||
|
If False, all attribute requests will be sent to the remote
|
||||||
|
process immediately and will block until a response is
|
||||||
|
received (or timeout has elapsed).
|
||||||
|
If True, requesting an attribute from the proxy returns a
|
||||||
|
new proxy immediately. The remote process is _not_ contacted
|
||||||
|
to make this request. This is faster, but it is possible to
|
||||||
|
request an attribute that does not exist on the proxied
|
||||||
|
object. In this case, AttributeError will not be raised
|
||||||
|
until an attempt is made to look up the attribute on the
|
||||||
|
remote process.
|
||||||
|
noProxyTypes List of object types that should _not_ be proxied when
|
||||||
|
sent to the remote process.
|
||||||
|
============= =============================================================
|
||||||
|
"""
|
||||||
|
self._proxyOptions.update(kwds)
|
||||||
|
|
||||||
|
def _getProxyOption(self, opt):
|
||||||
|
val = self._proxyOptions[opt]
|
||||||
|
if val is None:
|
||||||
|
return self._handler.getProxyOption(opt)
|
||||||
|
return val
|
||||||
|
|
||||||
|
def _getProxyOptions(self):
|
||||||
|
return {k: self._getProxyOption(k) for k in self._proxyOptions}
|
||||||
|
|
||||||
|
def __reduce__(self):
|
||||||
|
return (unpickleObjectProxy, (self._processId, self._proxyId, self._typeStr, self._attributes))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
#objRepr = self.__getattr__('__repr__')(callSync='value')
|
||||||
|
return "<ObjectProxy for process %d, object 0x%x: %s >" % (self._processId, self._proxyId, self._typeStr)
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
#if '_processId' not in self.__dict__:
|
||||||
|
#raise Exception("ObjectProxy has no processId")
|
||||||
|
#proc = Process._processes[self._processId]
|
||||||
|
deferred = self._getProxyOption('deferGetattr')
|
||||||
|
if deferred is True:
|
||||||
|
return self._deferredAttr(attr)
|
||||||
|
else:
|
||||||
|
opts = self._getProxyOptions()
|
||||||
|
return self._handler.getObjAttr(self, attr, **opts)
|
||||||
|
|
||||||
|
def _deferredAttr(self, attr):
|
||||||
|
return DeferredObjectProxy(self, attr)
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwds):
|
||||||
|
"""
|
||||||
|
Attempts to call the proxied object from the remote process.
|
||||||
|
Accepts extra keyword arguments:
|
||||||
|
|
||||||
|
_callSync 'off', 'sync', or 'async'
|
||||||
|
_returnType 'value', 'proxy', or 'auto'
|
||||||
|
|
||||||
|
"""
|
||||||
|
#opts = {}
|
||||||
|
#callSync = kwds.pop('_callSync', self.)
|
||||||
|
#if callSync is not None:
|
||||||
|
#opts['callSync'] = callSync
|
||||||
|
#returnType = kwds.pop('_returnType', self._defaultReturnValue)
|
||||||
|
#if returnType is not None:
|
||||||
|
#opts['returnType'] = returnType
|
||||||
|
opts = self._getProxyOptions()
|
||||||
|
for k in opts:
|
||||||
|
if '_'+k in kwds:
|
||||||
|
opts[k] = kwds.pop('_'+k)
|
||||||
|
#print "call", opts
|
||||||
|
return self._handler.callObj(obj=self, args=args, kwds=kwds, **opts)
|
||||||
|
|
||||||
|
def _getValue(self):
|
||||||
|
## this just gives us an easy way to change the behavior of the special methods
|
||||||
|
#proc = Process._processes[self._processId]
|
||||||
|
return self._handler.getObjValue(self)
|
||||||
|
|
||||||
|
|
||||||
|
## Explicitly proxy special methods. Is there a better way to do this??
|
||||||
|
|
||||||
|
def _getSpecialAttr(self, attr):
|
||||||
|
#return self.__getattr__(attr)
|
||||||
|
return self._deferredAttr(attr)
|
||||||
|
|
||||||
|
def __getitem__(self, *args):
|
||||||
|
return self._getSpecialAttr('__getitem__')(*args)
|
||||||
|
|
||||||
|
def __setitem__(self, *args):
|
||||||
|
return self._getSpecialAttr('__setitem__')(*args)
|
||||||
|
|
||||||
|
def __setattr__(self, *args):
|
||||||
|
return self._getSpecialAttr('__setattr__')(*args)
|
||||||
|
|
||||||
|
def __str__(self, *args):
|
||||||
|
return self._getSpecialAttr('__str__')(*args, _returnType=True)
|
||||||
|
|
||||||
|
def __len__(self, *args):
|
||||||
|
return self._getSpecialAttr('__len__')(*args)
|
||||||
|
|
||||||
|
def __add__(self, *args):
|
||||||
|
return self._getSpecialAttr('__add__')(*args)
|
||||||
|
|
||||||
|
def __sub__(self, *args):
|
||||||
|
return self._getSpecialAttr('__sub__')(*args)
|
||||||
|
|
||||||
|
def __div__(self, *args):
|
||||||
|
return self._getSpecialAttr('__div__')(*args)
|
||||||
|
|
||||||
|
def __mul__(self, *args):
|
||||||
|
return self._getSpecialAttr('__mul__')(*args)
|
||||||
|
|
||||||
|
def __pow__(self, *args):
|
||||||
|
return self._getSpecialAttr('__pow__')(*args)
|
||||||
|
|
||||||
|
def __rshift__(self, *args):
|
||||||
|
return self._getSpecialAttr('__rshift__')(*args)
|
||||||
|
|
||||||
|
def __lshift__(self, *args):
|
||||||
|
return self._getSpecialAttr('__lshift__')(*args)
|
||||||
|
|
||||||
|
def __floordiv__(self, *args):
|
||||||
|
return self._getSpecialAttr('__pow__')(*args)
|
||||||
|
|
||||||
|
def __eq__(self, *args):
|
||||||
|
return self._getSpecialAttr('__eq__')(*args)
|
||||||
|
|
||||||
|
def __ne__(self, *args):
|
||||||
|
return self._getSpecialAttr('__ne__')(*args)
|
||||||
|
|
||||||
|
def __lt__(self, *args):
|
||||||
|
return self._getSpecialAttr('__lt__')(*args)
|
||||||
|
|
||||||
|
def __gt__(self, *args):
|
||||||
|
return self._getSpecialAttr('__gt__')(*args)
|
||||||
|
|
||||||
|
def __le__(self, *args):
|
||||||
|
return self._getSpecialAttr('__le__')(*args)
|
||||||
|
|
||||||
|
def __ge__(self, *args):
|
||||||
|
return self._getSpecialAttr('__ge__')(*args)
|
||||||
|
|
||||||
|
def __and__(self, *args):
|
||||||
|
return self._getSpecialAttr('__and__')(*args)
|
||||||
|
|
||||||
|
def __or__(self, *args):
|
||||||
|
return self._getSpecialAttr('__or__')(*args)
|
||||||
|
|
||||||
|
def __xor__(self, *args):
|
||||||
|
return self._getSpecialAttr('__or__')(*args)
|
||||||
|
|
||||||
|
def __mod__(self, *args):
|
||||||
|
return self._getSpecialAttr('__mod__')(*args)
|
||||||
|
|
||||||
|
def __radd__(self, *args):
|
||||||
|
return self._getSpecialAttr('__radd__')(*args)
|
||||||
|
|
||||||
|
def __rsub__(self, *args):
|
||||||
|
return self._getSpecialAttr('__rsub__')(*args)
|
||||||
|
|
||||||
|
def __rdiv__(self, *args):
|
||||||
|
return self._getSpecialAttr('__rdiv__')(*args)
|
||||||
|
|
||||||
|
def __rmul__(self, *args):
|
||||||
|
return self._getSpecialAttr('__rmul__')(*args)
|
||||||
|
|
||||||
|
def __rpow__(self, *args):
|
||||||
|
return self._getSpecialAttr('__rpow__')(*args)
|
||||||
|
|
||||||
|
def __rrshift__(self, *args):
|
||||||
|
return self._getSpecialAttr('__rrshift__')(*args)
|
||||||
|
|
||||||
|
def __rlshift__(self, *args):
|
||||||
|
return self._getSpecialAttr('__rlshift__')(*args)
|
||||||
|
|
||||||
|
def __rfloordiv__(self, *args):
|
||||||
|
return self._getSpecialAttr('__rpow__')(*args)
|
||||||
|
|
||||||
|
def __rand__(self, *args):
|
||||||
|
return self._getSpecialAttr('__rand__')(*args)
|
||||||
|
|
||||||
|
def __ror__(self, *args):
|
||||||
|
return self._getSpecialAttr('__ror__')(*args)
|
||||||
|
|
||||||
|
def __rxor__(self, *args):
|
||||||
|
return self._getSpecialAttr('__ror__')(*args)
|
||||||
|
|
||||||
|
def __rmod__(self, *args):
|
||||||
|
return self._getSpecialAttr('__rmod__')(*args)
|
||||||
|
|
||||||
|
class DeferredObjectProxy(ObjectProxy):
|
||||||
|
def __init__(self, parentProxy, attribute):
|
||||||
|
## can't set attributes directly because setattr is overridden.
|
||||||
|
for k in ['_processId', '_typeStr', '_proxyId', '_handler']:
|
||||||
|
self.__dict__[k] = getattr(parentProxy, k)
|
||||||
|
self.__dict__['_parent'] = parentProxy ## make sure parent stays alive
|
||||||
|
self.__dict__['_attributes'] = parentProxy._attributes + (attribute,)
|
||||||
|
self.__dict__['_proxyOptions'] = parentProxy._proxyOptions.copy()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return ObjectProxy.__repr__(self) + '.' + '.'.join(self._attributes)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user