From 60a48ed2e4ef9fcecaffca33e009f4e0a35bd972 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 26 Apr 2018 13:22:47 -0700 Subject: [PATCH] reload tests pass in python 3 --- pyqtgraph/functions.py | 2 +- pyqtgraph/reload.py | 46 ++++++++++++++++-------- pyqtgraph/tests/test_reload.py | 65 ++++++++++++++++++++++++---------- 3 files changed, 79 insertions(+), 34 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 21e6c006..8857c052 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -2444,7 +2444,7 @@ def disconnect(signal, slot): try: signal.disconnect(slot) return True - except TypeError, RuntimeError: + except (TypeError, RuntimeError): slot = reload.getPreviousVersion(slot) if slot is None: return False diff --git a/pyqtgraph/reload.py b/pyqtgraph/reload.py index 5aa2ed68..f682a025 100644 --- a/pyqtgraph/reload.py +++ b/pyqtgraph/reload.py @@ -21,15 +21,18 @@ Does NOT: print module.someObject """ - +from __future__ import print_function import inspect, os, sys, gc, traceback, types try: - import __builtin__ as builtins + from builtins import reload as orig_reload except ImportError: - import builtins + from importlib import reload as orig_reload from .debug import printExc +py3 = sys.version_info >= (3,) + + def reloadAll(prefix=None, debug=False): """Automatically reload everything whose __file__ begins with prefix. - Skips reload if the file has not been updated (if .pyc is newer than .py) @@ -80,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 oldDict = module.__dict__.copy() - builtins.reload(module) + orig_reload(module) newDict = module.__dict__ ## Allow modules access to the old dictionary after they reload @@ -130,6 +133,9 @@ def updateFunction(old, new, debug, depth=0, visited=None): old.__code__ = new.__code__ old.__defaults__ = new.__defaults__ + if hasattr(old, '__kwdefaults'): + old.__kwdefaults__ = new.__kwdefaults__ + old.__doc__ = new.__doc__ if visited is None: visited = [] @@ -154,6 +160,8 @@ def updateFunction(old, new, debug, depth=0, visited=None): ## For classes: ## 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 + + def updateClass(old, new, debug): ## Track town all instances and subclasses of old refs = gc.get_referrers(old) @@ -198,7 +206,8 @@ def updateClass(old, new, debug): ## but it fixes a few specific cases (pyqt signals, for one) for attr in dir(old): 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: na = getattr(new, attr) except AttributeError: @@ -206,11 +215,14 @@ def updateClass(old, new, debug): print(" Skipping method update for %s; new class does not have this attribute" % attr) continue - if hasattr(oa, 'im_func') and hasattr(na, 'im_func') and oa.__func__ is not na.__func__: - depth = updateFunction(oa.__func__, na.__func__, debug) - if not hasattr(na.__func__, '__previous_reload_method__'): - na.__func__.__previous_reload_method__ = oa # important for managing signal connection - #oa.im_class = new ## bind old method to new class ## not allowed + ofunc = getattr(oa, '__func__', oa) # in py2 we have to get the __func__ from unbound method, + nfunc = getattr(na, '__func__', na) # in py3 the attribute IS the function + + 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: extra = "" if depth > 0: @@ -251,16 +263,22 @@ def getPreviousVersion(obj): if isinstance(obj, type) or inspect.isfunction(obj): return getattr(obj, '__previous_reload_version__', None) elif inspect.ismethod(obj): - if obj.im_self is None: + 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.im_self - cls = oldmethod.im_class - return types.MethodType(oldmethod.__func__, self, cls) + 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) diff --git a/pyqtgraph/tests/test_reload.py b/pyqtgraph/tests/test_reload.py index 7ceed546..57924d60 100644 --- a/pyqtgraph/tests/test_reload.py +++ b/pyqtgraph/tests/test_reload.py @@ -1,14 +1,23 @@ -import tempfile, os, sys, shutil, atexit +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 = tempfile.mkdtemp() -sys.path.insert(0, path) -def cleanup(): +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) -atexit.register(cleanup) + sys.path.remove(path) code = """ @@ -33,6 +42,8 @@ def remove_cache(mod): def test_reload(): + py3 = sys.version_info >= (3,) + # write a module mod = os.path.join(path, 'reload_test.py') open(mod, 'w').write(code.format(path=path, msg="C.fn() Version1")) @@ -42,7 +53,10 @@ def test_reload(): c = reload_test.C() c.sig.connect(c.fn) - v1 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + if py3: + v1 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, c.sig, c.fn, c.fn.__func__) + else: + v1 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) @@ -50,25 +64,34 @@ def test_reload(): open(mod, 'w').write(code.format(path=path, msg="C.fn() Version2")) remove_cache(mod) pg.reload.reloadAll(path, debug=True) - v2 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + if py3: + v2 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, c.sig, c.fn, c.fn.__func__) + else: + v2 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) - assert c.fn.im_class is v2[0] + 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.)") - assert oldcfn.im_class is v1[0] - assert oldcfn.im_func is v1[2].im_func - assert oldcfn.im_self is c - + 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=path, msg="C.fn() Version2")) remove_cache(mod) pg.reload.reloadAll(path, debug=True) - v3 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + if py3: + v3 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, c.sig, c.fn, c.fn.__func__) + else: + v3 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.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] @@ -76,11 +99,15 @@ def test_reload(): cfn1 = pg.reload.getPreviousVersion(c.fn) cfn2 = pg.reload.getPreviousVersion(cfn1) - assert cfn1.im_class is v2[0] - assert cfn1.im_func is v2[2].im_func - assert cfn1.im_self is c - assert cfn2.im_class is v1[0] - assert cfn2.im_func is v1[2].im_func - assert cfn2.im_self is c + 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 c.sig.disconnect(cfn2)