#!/usr/bin/env python # # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) # Copyright (c) 2008-2016 California Institute of Technology. # Copyright (c) 2016-2018 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/dill/blob/master/LICENSE """ Methods for detecting objects leading to pickling failures. """ import dis from inspect import ismethod, isfunction, istraceback, isframe, iscode from .pointers import parent, reference, at, parents, children from ._dill import _trace as trace from ._dill import PY3 __all__ = ['baditems','badobjects','badtypes','code','errors','freevars', 'getmodule','globalvars','nestedcode','nestedglobals','outermost', 'referredglobals','referrednested','trace','varnames'] def getmodule(object, _filename=None, force=False): """get the module of the object""" from inspect import getmodule as getmod module = getmod(object, _filename) if module or not force: return module if PY3: builtins = 'builtins' else: builtins = '__builtin__' builtins = __import__(builtins) from .source import getname name = getname(object, force=True) return builtins if name in vars(builtins).keys() else None def outermost(func): # is analogous to getsource(func,enclosing=True) """get outermost enclosing object (i.e. the outer function in a closure) NOTE: this is the object-equivalent of getsource(func, enclosing=True) """ if PY3: if ismethod(func): _globals = func.__func__.__globals__ or {} elif isfunction(func): _globals = func.__globals__ or {} else: return #XXX: or raise? no matches _globals = _globals.items() else: if ismethod(func): _globals = func.im_func.func_globals or {} elif isfunction(func): _globals = func.func_globals or {} else: return #XXX: or raise? no matches _globals = _globals.iteritems() # get the enclosing source from .source import getsourcelines try: lines,lnum = getsourcelines(func, enclosing=True) except: #TypeError, IOError lines,lnum = [],None code = ''.join(lines) # get all possible names,objects that are named in the enclosing source _locals = ((name,obj) for (name,obj) in _globals if name in code) # now only save the objects that generate the enclosing block for name,obj in _locals: #XXX: don't really need 'name' try: if getsourcelines(obj) == (lines,lnum): return obj except: #TypeError, IOError pass return #XXX: or raise? no matches def nestedcode(func, recurse=True): #XXX: or return dict of {co_name: co} ? """get the code objects for any nested functions (e.g. in a closure)""" func = code(func) if not iscode(func): return [] #XXX: or raise? no matches nested = set() for co in func.co_consts: if co is None: continue co = code(co) if co: nested.add(co) if recurse: nested |= set(nestedcode(co, recurse=True)) return list(nested) def code(func): '''get the code object for the given function or method NOTE: use dill.source.getsource(CODEOBJ) to get the source code ''' if PY3: im_func = '__func__' func_code = '__code__' else: im_func = 'im_func' func_code = 'func_code' if ismethod(func): func = getattr(func, im_func) if isfunction(func): func = getattr(func, func_code) if istraceback(func): func = func.tb_frame if isframe(func): func = func.f_code if iscode(func): return func return #XXX: ugly: parse dis.dis for name after " len(referrednested(func)), try calling func(). If possible, python builds code objects, but delays building functions until func() is called. """ if PY3: att1 = '__code__' att0 = '__func__' else: att1 = 'func_code' # functions att0 = 'im_func' # methods import gc funcs = set() # get the code objects, and try to track down by referrence for co in nestedcode(func, recurse): # look for function objects that refer to the code object for obj in gc.get_referrers(co): # get methods _ = getattr(obj, att0, None) # ismethod if getattr(_, att1, None) is co: funcs.add(obj) # get functions elif getattr(obj, att1, None) is co: funcs.add(obj) # get frame objects elif getattr(obj, 'f_code', None) is co: funcs.add(obj) # get code objects elif hasattr(obj, 'co_code') and obj is co: funcs.add(obj) # frameobjs => func.func_code.co_varnames not in func.func_code.co_cellvars # funcobjs => func.func_code.co_cellvars not in func.func_code.co_varnames # frameobjs are not found, however funcobjs are... # (see: test_mixins.quad ... and test_mixins.wtf) # after execution, code objects get compiled, and then may be found by gc return list(funcs) def freevars(func): """get objects defined in enclosing code that are referred to by func returns a dict of {name:object}""" if PY3: im_func = '__func__' func_code = '__code__' func_closure = '__closure__' else: im_func = 'im_func' func_code = 'func_code' func_closure = 'func_closure' if ismethod(func): func = getattr(func, im_func) if isfunction(func): closures = getattr(func, func_closure) or () func = getattr(func, func_code).co_freevars # get freevars else: return {} return dict((name,c.cell_contents) for (name,c) in zip(func,closures)) # thanks to Davies Liu for recursion of globals def nestedglobals(func, recurse=True): """get the names of any globals found within func""" func = code(func) if func is None: return list() from .temp import capture names = set() with capture('stdout') as out: dis.dis(func) #XXX: dis.dis(None) disassembles last traceback for line in out.getvalue().splitlines(): if '_GLOBAL' in line: name = line.split('(')[-1].split(')')[0] names.add(name) for co in getattr(func, 'co_consts', tuple()): if co and recurse and iscode(co): names.update(nestedglobals(co, recurse=True)) return list(names) def referredglobals(func, recurse=True, builtin=False): """get the names of objects in the global scope referred to by func""" return globalvars(func, recurse, builtin).keys() def globalvars(func, recurse=True, builtin=False): """get objects defined in global scope that are referred to by func return a dict of {name:object}""" if PY3: im_func = '__func__' func_code = '__code__' func_globals = '__globals__' func_closure = '__closure__' else: im_func = 'im_func' func_code = 'func_code' func_globals = 'func_globals' func_closure = 'func_closure' if ismethod(func): func = getattr(func, im_func) if isfunction(func): globs = vars(getmodule(sum)).copy() if builtin else {} # get references from within closure orig_func, func = func, set() for obj in getattr(orig_func, func_closure) or {}: _vars = globalvars(obj.cell_contents, recurse, builtin) or {} func.update(_vars) #XXX: (above) be wary of infinte recursion? globs.update(_vars) # get globals globs.update(getattr(orig_func, func_globals) or {}) # get names of references if not recurse: func.update(getattr(orig_func, func_code).co_names) else: func.update(nestedglobals(getattr(orig_func, func_code))) # find globals for all entries of func for key in func.copy(): #XXX: unnecessary...? nested_func = globs.get(key) if nested_func == orig_func: #func.remove(key) if key in func else None continue #XXX: globalvars(func, False)? func.update(globalvars(nested_func, True, builtin)) elif iscode(func): globs = vars(getmodule(sum)).copy() if builtin else {} #globs.update(globals()) if not recurse: func = func.co_names # get names else: orig_func = func.co_name # to stop infinite recursion func = set(nestedglobals(func)) # find globals for all entries of func for key in func.copy(): #XXX: unnecessary...? if key == orig_func: #func.remove(key) if key in func else None continue #XXX: globalvars(func, False)? nested_func = globs.get(key) func.update(globalvars(nested_func, True, builtin)) else: return {} #NOTE: if name not in func_globals, then we skip it... return dict((name,globs[name]) for name in func if name in globs) def varnames(func): """get names of variables defined by func returns a tuple (local vars, local vars referrenced by nested functions)""" func = code(func) if not iscode(func): return () #XXX: better ((),())? or None? return func.co_varnames, func.co_cellvars def baditems(obj, exact=False, safe=False): #XXX: obj=globals() ? """get items in object that fail to pickle""" if not hasattr(obj,'__iter__'): # is not iterable return [j for j in (badobjects(obj,0,exact,safe),) if j is not None] obj = obj.values() if getattr(obj,'values',None) else obj _obj = [] # can't use a set, as items may be unhashable [_obj.append(badobjects(i,0,exact,safe)) for i in obj if i not in _obj] return [j for j in _obj if j is not None] def badobjects(obj, depth=0, exact=False, safe=False): """get objects that fail to pickle""" from dill import pickles if not depth: if pickles(obj,exact,safe): return None return obj return dict(((attr, badobjects(getattr(obj,attr),depth-1,exact,safe)) \ for attr in dir(obj) if not pickles(getattr(obj,attr),exact,safe))) def badtypes(obj, depth=0, exact=False, safe=False): """get types for objects that fail to pickle""" from dill import pickles if not depth: if pickles(obj,exact,safe): return None return type(obj) return dict(((attr, badtypes(getattr(obj,attr),depth-1,exact,safe)) \ for attr in dir(obj) if not pickles(getattr(obj,attr),exact,safe))) def errors(obj, depth=0, exact=False, safe=False): """get errors for objects that fail to pickle""" from dill import pickles, copy if not depth: try: pik = copy(obj) if exact: assert pik == obj, \ "Unpickling produces %s instead of %s" % (pik,obj) assert type(pik) == type(obj), \ "Unpickling produces %s instead of %s" % (type(pik),type(obj)) return None except Exception: import sys return sys.exc_info()[1] _dict = {} for attr in dir(obj): try: _attr = getattr(obj,attr) except Exception: import sys _dict[attr] = sys.exc_info()[1] continue if not pickles(_attr,exact,safe): _dict[attr] = errors(_attr,depth-1,exact,safe) return _dict # EOF