commit
e8a999d8b1
@ -14,7 +14,8 @@ import sys, struct
|
|||||||
from .python2_3 import asUnicode, basestring
|
from .python2_3 import asUnicode, basestring
|
||||||
from .Qt import QtGui, QtCore, USE_PYSIDE
|
from .Qt import QtGui, QtCore, USE_PYSIDE
|
||||||
from . import getConfigOption, setConfigOptions
|
from . import getConfigOption, setConfigOptions
|
||||||
from . import debug
|
from . import debug, reload
|
||||||
|
from .reload import getPreviousVersion
|
||||||
from .metaarray import MetaArray
|
from .metaarray import MetaArray
|
||||||
|
|
||||||
|
|
||||||
@ -2428,3 +2429,45 @@ def toposort(deps, nodes=None, seen=None, stack=None, depth=0):
|
|||||||
sorted.extend( toposort(deps, deps[n], seen, stack+[n], depth=depth+1))
|
sorted.extend( toposort(deps, deps[n], seen, stack+[n], depth=depth+1))
|
||||||
sorted.append(n)
|
sorted.append(n)
|
||||||
return sorted
|
return sorted
|
||||||
|
|
||||||
|
|
||||||
|
def disconnect(signal, slot):
|
||||||
|
"""Disconnect a Qt signal from a slot.
|
||||||
|
|
||||||
|
This method augments Qt's Signal.disconnect():
|
||||||
|
|
||||||
|
* Return bool indicating whether disconnection was successful, rather than
|
||||||
|
raising an exception
|
||||||
|
* Attempt to disconnect prior versions of the slot when using pg.reload
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
signal.disconnect(slot)
|
||||||
|
return True
|
||||||
|
except (TypeError, RuntimeError):
|
||||||
|
slot = reload.getPreviousVersion(slot)
|
||||||
|
if slot is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class SignalBlock(object):
|
||||||
|
"""Class used to temporarily block a Qt signal connection::
|
||||||
|
|
||||||
|
with SignalBlock(signal, slot):
|
||||||
|
# do something that emits a signal; it will
|
||||||
|
# not be delivered to slot
|
||||||
|
"""
|
||||||
|
def __init__(self, signal, slot):
|
||||||
|
self.signal = signal
|
||||||
|
self.slot = slot
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.reconnect = disconnect(self.signal, self.slot)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
if self.reconnect:
|
||||||
|
self.signal.connect(self.slot)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,13 +21,17 @@ Does NOT:
|
|||||||
print module.someObject
|
print module.someObject
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
import inspect, os, sys, gc, traceback
|
import inspect, os, sys, gc, traceback, types
|
||||||
try:
|
|
||||||
import __builtin__ as builtins
|
|
||||||
except ImportError:
|
|
||||||
import builtins
|
|
||||||
from .debug import printExc
|
from .debug import printExc
|
||||||
|
try:
|
||||||
|
from importlib import reload as orig_reload
|
||||||
|
except ImportError:
|
||||||
|
orig_reload = reload
|
||||||
|
|
||||||
|
|
||||||
|
py3 = sys.version_info >= (3,)
|
||||||
|
|
||||||
|
|
||||||
def reloadAll(prefix=None, debug=False):
|
def reloadAll(prefix=None, debug=False):
|
||||||
"""Automatically reload everything whose __file__ begins with prefix.
|
"""Automatically reload everything whose __file__ begins with prefix.
|
||||||
@ -79,7 +83,7 @@ def reload(module, debug=False, lists=False, dicts=False):
|
|||||||
|
|
||||||
## make a copy of the old module dictionary, reload, then grab the new module dictionary for comparison
|
## make a copy of the old module dictionary, reload, then grab the new module dictionary for comparison
|
||||||
oldDict = module.__dict__.copy()
|
oldDict = module.__dict__.copy()
|
||||||
builtins.reload(module)
|
orig_reload(module)
|
||||||
newDict = module.__dict__
|
newDict = module.__dict__
|
||||||
|
|
||||||
## Allow modules access to the old dictionary after they reload
|
## Allow modules access to the old dictionary after they reload
|
||||||
@ -97,6 +101,8 @@ def reload(module, debug=False, lists=False, dicts=False):
|
|||||||
if debug:
|
if debug:
|
||||||
print(" Updating class %s.%s (0x%x -> 0x%x)" % (module.__name__, k, id(old), id(new)))
|
print(" Updating class %s.%s (0x%x -> 0x%x)" % (module.__name__, k, id(old), id(new)))
|
||||||
updateClass(old, new, debug)
|
updateClass(old, new, debug)
|
||||||
|
# don't put this inside updateClass because it is reentrant.
|
||||||
|
new.__previous_reload_version__ = old
|
||||||
|
|
||||||
elif inspect.isfunction(old):
|
elif inspect.isfunction(old):
|
||||||
depth = updateFunction(old, new, debug)
|
depth = updateFunction(old, new, debug)
|
||||||
@ -127,6 +133,9 @@ def updateFunction(old, new, debug, depth=0, visited=None):
|
|||||||
|
|
||||||
old.__code__ = new.__code__
|
old.__code__ = new.__code__
|
||||||
old.__defaults__ = new.__defaults__
|
old.__defaults__ = new.__defaults__
|
||||||
|
if hasattr(old, '__kwdefaults'):
|
||||||
|
old.__kwdefaults__ = new.__kwdefaults__
|
||||||
|
old.__doc__ = new.__doc__
|
||||||
|
|
||||||
if visited is None:
|
if visited is None:
|
||||||
visited = []
|
visited = []
|
||||||
@ -151,8 +160,9 @@ def updateFunction(old, new, debug, depth=0, visited=None):
|
|||||||
## For classes:
|
## For classes:
|
||||||
## 1) find all instances of the old class and set instance.__class__ to the new class
|
## 1) find all instances of the old class and set instance.__class__ to the new class
|
||||||
## 2) update all old class methods to use code from the new class methods
|
## 2) update all old class methods to use code from the new class methods
|
||||||
def updateClass(old, new, debug):
|
|
||||||
|
|
||||||
|
|
||||||
|
def updateClass(old, new, debug):
|
||||||
## Track town all instances and subclasses of old
|
## Track town all instances and subclasses of old
|
||||||
refs = gc.get_referrers(old)
|
refs = gc.get_referrers(old)
|
||||||
for ref in refs:
|
for ref in refs:
|
||||||
@ -174,13 +184,20 @@ def updateClass(old, new, debug):
|
|||||||
## This seems to work. Is there any reason not to?
|
## This seems to work. Is there any reason not to?
|
||||||
## Note that every time we reload, the class hierarchy becomes more complex.
|
## Note that every time we reload, the class hierarchy becomes more complex.
|
||||||
## (and I presume this may slow things down?)
|
## (and I presume this may slow things down?)
|
||||||
ref.__bases__ = ref.__bases__[:ind] + (new,old) + ref.__bases__[ind+1:]
|
newBases = ref.__bases__[:ind] + (new,old) + ref.__bases__[ind+1:]
|
||||||
|
try:
|
||||||
|
ref.__bases__ = newBases
|
||||||
|
except TypeError:
|
||||||
|
print(" Error setting bases for class %s" % ref)
|
||||||
|
print(" old bases: %s" % repr(ref.__bases__))
|
||||||
|
print(" new bases: %s" % repr(newBases))
|
||||||
|
raise
|
||||||
if debug:
|
if debug:
|
||||||
print(" Changed superclass for %s" % safeStr(ref))
|
print(" Changed superclass for %s" % safeStr(ref))
|
||||||
#else:
|
#else:
|
||||||
#if debug:
|
#if debug:
|
||||||
#print " Ignoring reference", type(ref)
|
#print " Ignoring reference", type(ref)
|
||||||
except:
|
except Exception:
|
||||||
print("Error updating reference (%s) for class change (%s -> %s)" % (safeStr(ref), safeStr(old), safeStr(new)))
|
print("Error updating reference (%s) for class change (%s -> %s)" % (safeStr(ref), safeStr(old), safeStr(new)))
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@ -189,7 +206,8 @@ def updateClass(old, new, debug):
|
|||||||
## but it fixes a few specific cases (pyqt signals, for one)
|
## but it fixes a few specific cases (pyqt signals, for one)
|
||||||
for attr in dir(old):
|
for attr in dir(old):
|
||||||
oa = getattr(old, attr)
|
oa = getattr(old, attr)
|
||||||
if inspect.ismethod(oa):
|
if (py3 and inspect.isfunction(oa)) or inspect.ismethod(oa):
|
||||||
|
# note python2 has unbound methods, whereas python3 just uses plain functions
|
||||||
try:
|
try:
|
||||||
na = getattr(new, attr)
|
na = getattr(new, attr)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@ -197,9 +215,14 @@ def updateClass(old, new, debug):
|
|||||||
print(" Skipping method update for %s; new class does not have this attribute" % attr)
|
print(" Skipping method update for %s; new class does not have this attribute" % attr)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if hasattr(oa, 'im_func') and hasattr(na, 'im_func') and oa.__func__ is not na.__func__:
|
ofunc = getattr(oa, '__func__', oa) # in py2 we have to get the __func__ from unbound method,
|
||||||
depth = updateFunction(oa.__func__, na.__func__, debug)
|
nfunc = getattr(na, '__func__', na) # in py3 the attribute IS the function
|
||||||
#oa.im_class = new ## bind old method to new class ## not allowed
|
|
||||||
|
if ofunc is not nfunc:
|
||||||
|
depth = updateFunction(ofunc, nfunc, debug)
|
||||||
|
if not hasattr(nfunc, '__previous_reload_method__'):
|
||||||
|
nfunc.__previous_reload_method__ = oa # important for managing signal connection
|
||||||
|
#oa.__class__ = new ## bind old method to new class ## not allowed
|
||||||
if debug:
|
if debug:
|
||||||
extra = ""
|
extra = ""
|
||||||
if depth > 0:
|
if depth > 0:
|
||||||
@ -208,6 +231,8 @@ def updateClass(old, new, debug):
|
|||||||
|
|
||||||
## And copy in new functions that didn't exist previously
|
## And copy in new functions that didn't exist previously
|
||||||
for attr in dir(new):
|
for attr in dir(new):
|
||||||
|
if attr == '__previous_reload_version__':
|
||||||
|
continue
|
||||||
if not hasattr(old, attr):
|
if not hasattr(old, attr):
|
||||||
if debug:
|
if debug:
|
||||||
print(" Adding missing attribute %s" % attr)
|
print(" Adding missing attribute %s" % attr)
|
||||||
@ -223,14 +248,37 @@ def updateClass(old, new, debug):
|
|||||||
def safeStr(obj):
|
def safeStr(obj):
|
||||||
try:
|
try:
|
||||||
s = str(obj)
|
s = str(obj)
|
||||||
except:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
s = repr(obj)
|
s = repr(obj)
|
||||||
except:
|
except Exception:
|
||||||
s = "<instance of %s at 0x%x>" % (safeStr(type(obj)), id(obj))
|
s = "<instance of %s at 0x%x>" % (safeStr(type(obj)), id(obj))
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def getPreviousVersion(obj):
|
||||||
|
"""Return the previous version of *obj*, or None if this object has not
|
||||||
|
been reloaded.
|
||||||
|
"""
|
||||||
|
if isinstance(obj, type) or inspect.isfunction(obj):
|
||||||
|
return getattr(obj, '__previous_reload_version__', None)
|
||||||
|
elif inspect.ismethod(obj):
|
||||||
|
if obj.__self__ is None:
|
||||||
|
# unbound method
|
||||||
|
return getattr(obj.__func__, '__previous_reload_method__', None)
|
||||||
|
else:
|
||||||
|
oldmethod = getattr(obj.__func__, '__previous_reload_method__', None)
|
||||||
|
if oldmethod is None:
|
||||||
|
return None
|
||||||
|
self = obj.__self__
|
||||||
|
oldfunc = getattr(oldmethod, '__func__', oldmethod)
|
||||||
|
if hasattr(oldmethod, 'im_class'):
|
||||||
|
# python 2
|
||||||
|
cls = oldmethod.im_class
|
||||||
|
return types.MethodType(oldfunc, self, cls)
|
||||||
|
else:
|
||||||
|
# python 3
|
||||||
|
return types.MethodType(oldfunc, self)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
116
pyqtgraph/tests/test_reload.py
Normal file
116
pyqtgraph/tests/test_reload.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import tempfile, os, sys, shutil
|
||||||
|
import pyqtgraph as pg
|
||||||
|
import pyqtgraph.reload
|
||||||
|
|
||||||
|
|
||||||
|
pgpath = os.path.join(os.path.dirname(pg.__file__), '..')
|
||||||
|
|
||||||
|
# make temporary directory to write module code
|
||||||
|
path = None
|
||||||
|
|
||||||
|
def setup_module():
|
||||||
|
# make temporary directory to write module code
|
||||||
|
global path
|
||||||
|
path = tempfile.mkdtemp()
|
||||||
|
sys.path.insert(0, path)
|
||||||
|
|
||||||
|
def teardown_module():
|
||||||
|
global path
|
||||||
|
shutil.rmtree(path)
|
||||||
|
sys.path.remove(path)
|
||||||
|
|
||||||
|
|
||||||
|
code = """
|
||||||
|
import sys
|
||||||
|
sys.path.append('{path}')
|
||||||
|
|
||||||
|
import pyqtgraph as pg
|
||||||
|
|
||||||
|
class C(pg.QtCore.QObject):
|
||||||
|
sig = pg.QtCore.Signal()
|
||||||
|
def fn(self):
|
||||||
|
print("{msg}")
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def remove_cache(mod):
|
||||||
|
if os.path.isfile(mod+'c'):
|
||||||
|
os.remove(mod+'c')
|
||||||
|
cachedir = os.path.join(os.path.dirname(mod), '__pycache__')
|
||||||
|
if os.path.isdir(cachedir):
|
||||||
|
shutil.rmtree(cachedir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_reload():
|
||||||
|
py3 = sys.version_info >= (3,)
|
||||||
|
|
||||||
|
# write a module
|
||||||
|
mod = os.path.join(path, 'reload_test_mod.py')
|
||||||
|
print("\nRELOAD FILE:", mod)
|
||||||
|
open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version1"))
|
||||||
|
|
||||||
|
# import the new module
|
||||||
|
import reload_test_mod
|
||||||
|
print("RELOAD MOD:", reload_test_mod.__file__)
|
||||||
|
|
||||||
|
c = reload_test_mod.C()
|
||||||
|
c.sig.connect(c.fn)
|
||||||
|
if py3:
|
||||||
|
v1 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, c.sig, c.fn, c.fn.__func__)
|
||||||
|
else:
|
||||||
|
v1 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, reload_test_mod.C.fn.__func__, c.sig, c.fn, c.fn.__func__)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# write again and reload
|
||||||
|
open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version2"))
|
||||||
|
remove_cache(mod)
|
||||||
|
pg.reload.reloadAll(path, debug=True)
|
||||||
|
if py3:
|
||||||
|
v2 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, c.sig, c.fn, c.fn.__func__)
|
||||||
|
else:
|
||||||
|
v2 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, reload_test_mod.C.fn.__func__, c.sig, c.fn, c.fn.__func__)
|
||||||
|
|
||||||
|
if not py3:
|
||||||
|
assert c.fn.im_class is v2[0]
|
||||||
|
oldcfn = pg.reload.getPreviousVersion(c.fn)
|
||||||
|
if oldcfn is None:
|
||||||
|
# Function did not reload; are we using pytest's assertion rewriting?
|
||||||
|
raise Exception("Function did not reload. (This can happen when using py.test"
|
||||||
|
" with assertion rewriting; use --assert=plain for this test.)")
|
||||||
|
if py3:
|
||||||
|
assert oldcfn.__func__ is v1[2]
|
||||||
|
else:
|
||||||
|
assert oldcfn.im_class is v1[0]
|
||||||
|
assert oldcfn.__func__ is v1[2].__func__
|
||||||
|
assert oldcfn.__self__ is c
|
||||||
|
|
||||||
|
|
||||||
|
# write again and reload
|
||||||
|
open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version2"))
|
||||||
|
remove_cache(mod)
|
||||||
|
pg.reload.reloadAll(path, debug=True)
|
||||||
|
if py3:
|
||||||
|
v3 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, c.sig, c.fn, c.fn.__func__)
|
||||||
|
else:
|
||||||
|
v3 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, reload_test_mod.C.fn.__func__, c.sig, c.fn, c.fn.__func__)
|
||||||
|
|
||||||
|
#for i in range(len(old)):
|
||||||
|
#print id(old[i]), id(new1[i]), id(new2[i]), old[i], new1[i]
|
||||||
|
|
||||||
|
cfn1 = pg.reload.getPreviousVersion(c.fn)
|
||||||
|
cfn2 = pg.reload.getPreviousVersion(cfn1)
|
||||||
|
|
||||||
|
if py3:
|
||||||
|
assert cfn1.__func__ is v2[2]
|
||||||
|
assert cfn2.__func__ is v1[2]
|
||||||
|
else:
|
||||||
|
assert cfn1.__func__ is v2[2].__func__
|
||||||
|
assert cfn2.__func__ is v1[2].__func__
|
||||||
|
assert cfn1.im_class is v2[0]
|
||||||
|
assert cfn2.im_class is v1[0]
|
||||||
|
assert cfn1.__self__ is c
|
||||||
|
assert cfn2.__self__ is c
|
||||||
|
|
||||||
|
pg.functions.disconnect(c.sig, c.fn)
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user