|
#!/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
|
|
#
|
|
# inspired by inspect.py from Python-2.7.6
|
|
# inspect.py author: 'Ka-Ping Yee <ping@lfw.org>'
|
|
# inspect.py merged into original dill.source by Mike McKerns 4/13/14
|
|
"""
|
|
Extensions to python's 'inspect' module, which can be used
|
|
to retrieve information from live python objects. The methods
|
|
defined in this module are augmented to facilitate access to
|
|
source code of interactively defined functions and classes,
|
|
as well as provide access to source code for objects defined
|
|
in a file.
|
|
"""
|
|
|
|
__all__ = ['findsource', 'getsourcelines', 'getsource', 'indent', 'outdent', \
|
|
'_wrap', 'dumpsource', 'getname', '_namespace', 'getimport', \
|
|
'_importable', 'importable','isdynamic', 'isfrommain']
|
|
|
|
import re
|
|
import linecache
|
|
from tokenize import TokenError
|
|
from inspect import ismodule, isclass, ismethod, isfunction, istraceback
|
|
from inspect import isframe, iscode, getfile, getmodule, getsourcefile
|
|
from inspect import getblock, indentsize, isbuiltin
|
|
from ._dill import PY3
|
|
|
|
def isfrommain(obj):
|
|
"check if object was built in __main__"
|
|
module = getmodule(obj)
|
|
if module and module.__name__ == '__main__':
|
|
return True
|
|
return False
|
|
|
|
|
|
def isdynamic(obj):
|
|
"check if object was built in the interpreter"
|
|
try: file = getfile(obj)
|
|
except TypeError: file = None
|
|
if file == '<stdin>' and isfrommain(obj):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _matchlambda(func, line):
|
|
"""check if lambda object 'func' matches raw line of code 'line'"""
|
|
from .detect import code as getcode
|
|
from .detect import freevars, globalvars, varnames
|
|
dummy = lambda : '__this_is_a_big_dummy_function__'
|
|
# process the line (removing leading whitespace, etc)
|
|
lhs,rhs = line.split('lambda ',1)[-1].split(":", 1) #FIXME: if !1 inputs
|
|
try: #FIXME: unsafe
|
|
_ = eval("lambda %s : %s" % (lhs,rhs), globals(),locals())
|
|
except: _ = dummy
|
|
# get code objects, for comparison
|
|
_, code = getcode(_).co_code, getcode(func).co_code
|
|
# check if func is in closure
|
|
_f = [line.count(i) for i in freevars(func).keys()]
|
|
if not _f: # not in closure
|
|
# check if code matches
|
|
if _ == code: return True
|
|
return False
|
|
# weak check on freevars
|
|
if not all(_f): return False #XXX: VERY WEAK
|
|
# weak check on varnames and globalvars
|
|
_f = varnames(func)
|
|
_f = [line.count(i) for i in _f[0]+_f[1]]
|
|
if _f and not all(_f): return False #XXX: VERY WEAK
|
|
_f = [line.count(i) for i in globalvars(func).keys()]
|
|
if _f and not all(_f): return False #XXX: VERY WEAK
|
|
# check if func is a double lambda
|
|
if (line.count('lambda ') > 1) and (lhs in freevars(func).keys()):
|
|
_lhs,_rhs = rhs.split('lambda ',1)[-1].split(":",1) #FIXME: if !1 inputs
|
|
try: #FIXME: unsafe
|
|
_f = eval("lambda %s : %s" % (_lhs,_rhs), globals(),locals())
|
|
except: _f = dummy
|
|
# get code objects, for comparison
|
|
_, code = getcode(_f).co_code, getcode(func).co_code
|
|
if len(_) != len(code): return False
|
|
#NOTE: should be same code same order, but except for 't' and '\x88'
|
|
_ = set((i,j) for (i,j) in zip(_,code) if i != j)
|
|
if len(_) != 1: return False #('t','\x88')
|
|
return True
|
|
# check indentsize
|
|
if not indentsize(line): return False #FIXME: is this a good check???
|
|
# check if code 'pattern' matches
|
|
#XXX: or pattern match against dis.dis(code)? (or use uncompyle2?)
|
|
_ = _.split(_[0]) # 't' #XXX: remove matching values if starts the same?
|
|
_f = code.split(code[0]) # '\x88'
|
|
#NOTE: should be same code different order, with different first element
|
|
_ = dict(re.match('([\W\D\S])(.*)', _[i]).groups() for i in range(1,len(_)))
|
|
_f = dict(re.match('([\W\D\S])(.*)', _f[i]).groups() for i in range(1,len(_f)))
|
|
if (_.keys() == _f.keys()) and (sorted(_.values()) == sorted(_f.values())):
|
|
return True
|
|
return False
|
|
|
|
|
|
def findsource(object):
|
|
"""Return the entire source file and starting line number for an object.
|
|
For interactively-defined objects, the 'file' is the interpreter's history.
|
|
|
|
The argument may be a module, class, method, function, traceback, frame,
|
|
or code object. The source code is returned as a list of all the lines
|
|
in the file and the line number indexes a line in that list. An IOError
|
|
is raised if the source code cannot be retrieved, while a TypeError is
|
|
raised for objects where the source code is unavailable (e.g. builtins)."""
|
|
|
|
module = getmodule(object)
|
|
try: file = getfile(module)
|
|
except TypeError: file = None
|
|
# use readline when working in interpreter (i.e. __main__ and not file)
|
|
if module and module.__name__ == '__main__' and not file:
|
|
import readline
|
|
lbuf = readline.get_current_history_length()
|
|
lines = [readline.get_history_item(i)+'\n' for i in range(1,lbuf)]
|
|
else:
|
|
try: # special handling for class instances
|
|
if not isclass(object) and isclass(type(object)): # __class__
|
|
file = getfile(module)
|
|
sourcefile = getsourcefile(module)
|
|
else: # builtins fail with a TypeError
|
|
file = getfile(object)
|
|
sourcefile = getsourcefile(object)
|
|
except (TypeError, AttributeError): # fail with better error
|
|
file = getfile(object)
|
|
sourcefile = getsourcefile(object)
|
|
if not sourcefile and file[:1] + file[-1:] != '<>':
|
|
raise IOError('source code not available')
|
|
file = sourcefile if sourcefile else file
|
|
|
|
module = getmodule(object, file)
|
|
if module:
|
|
lines = linecache.getlines(file, module.__dict__)
|
|
else:
|
|
lines = linecache.getlines(file)
|
|
|
|
if not lines:
|
|
raise IOError('could not get source code')
|
|
|
|
#FIXME: all below may fail if exec used (i.e. exec('f = lambda x:x') )
|
|
if ismodule(object):
|
|
return lines, 0
|
|
|
|
name = pat1 = obj = ''
|
|
pat2 = r'^(\s*@)'
|
|
# pat1b = r'^(\s*%s\W*=)' % name #FIXME: finds 'f = decorate(f)', not exec
|
|
if ismethod(object):
|
|
name = object.__name__
|
|
if name == '<lambda>': pat1 = r'(.*(?<!\w)lambda(:|\s))'
|
|
else: pat1 = r'^(\s*def\s)'
|
|
if PY3: object = object.__func__
|
|
else: object = object.im_func
|
|
if isfunction(object):
|
|
name = object.__name__
|
|
if name == '<lambda>':
|
|
pat1 = r'(.*(?<!\w)lambda(:|\s))'
|
|
obj = object #XXX: better a copy?
|
|
else: pat1 = r'^(\s*def\s)'
|
|
if PY3: object = object.__code__
|
|
else: object = object.func_code
|
|
if istraceback(object):
|
|
object = object.tb_frame
|
|
if isframe(object):
|
|
object = object.f_code
|
|
if iscode(object):
|
|
if not hasattr(object, 'co_firstlineno'):
|
|
raise IOError('could not find function definition')
|
|
stdin = object.co_filename == '<stdin>'
|
|
if stdin:
|
|
lnum = len(lines) - 1 # can't get lnum easily, so leverage pat
|
|
if not pat1: pat1 = r'^(\s*def\s)|(.*(?<!\w)lambda(:|\s))|^(\s*@)'
|
|
else:
|
|
lnum = object.co_firstlineno - 1
|
|
pat1 = r'^(\s*def\s)|(.*(?<!\w)lambda(:|\s))|^(\s*@)'
|
|
pat1 = re.compile(pat1); pat2 = re.compile(pat2)
|
|
#XXX: candidate_lnum = [n for n in range(lnum) if pat1.match(lines[n])]
|
|
while lnum > 0: #XXX: won't find decorators in <stdin> ?
|
|
line = lines[lnum]
|
|
if pat1.match(line):
|
|
if not stdin: break # co_firstlineno does the job
|
|
if name == '<lambda>': # hackery needed to confirm a match
|
|
if _matchlambda(obj, line): break
|
|
else: # not a lambda, just look for the name
|
|
if name in line: # need to check for decorator...
|
|
hats = 0
|
|
for _lnum in range(lnum-1,-1,-1):
|
|
if pat2.match(lines[_lnum]): hats += 1
|
|
else: break
|
|
lnum = lnum - hats
|
|
break
|
|
lnum = lnum - 1
|
|
return lines, lnum
|
|
|
|
try: # turn instances into classes
|
|
if not isclass(object) and isclass(type(object)): # __class__
|
|
object = object.__class__ #XXX: sometimes type(class) is better?
|
|
#XXX: we don't find how the instance was built
|
|
except AttributeError: pass
|
|
if isclass(object):
|
|
name = object.__name__
|
|
pat = re.compile(r'^(\s*)class\s*' + name + r'\b')
|
|
# make some effort to find the best matching class definition:
|
|
# use the one with the least indentation, which is the one
|
|
# that's most probably not inside a function definition.
|
|
candidates = []
|
|
for i in range(len(lines)-1,-1,-1):
|
|
match = pat.match(lines[i])
|
|
if match:
|
|
# if it's at toplevel, it's already the best one
|
|
if lines[i][0] == 'c':
|
|
return lines, i
|
|
# else add whitespace to candidate list
|
|
candidates.append((match.group(1), i))
|
|
if candidates:
|
|
# this will sort by whitespace, and by line number,
|
|
# less whitespace first #XXX: should sort high lnum before low
|
|
candidates.sort()
|
|
return lines, candidates[0][1]
|
|
else:
|
|
raise IOError('could not find class definition')
|
|
raise IOError('could not find code object')
|
|
|
|
|
|
def getblocks(object, lstrip=False, enclosing=False, locate=False):
|
|
"""Return a list of source lines and starting line number for an object.
|
|
Interactively-defined objects refer to lines in the interpreter's history.
|
|
|
|
If enclosing=True, then also return any enclosing code.
|
|
If lstrip=True, ensure there is no indentation in the first line of code.
|
|
If locate=True, then also return the line number for the block of code.
|
|
|
|
DEPRECATED: use 'getsourcelines' instead
|
|
"""
|
|
lines, lnum = findsource(object)
|
|
|
|
if ismodule(object):
|
|
if lstrip: lines = _outdent(lines)
|
|
return ([lines], [0]) if locate is True else [lines]
|
|
|
|
#XXX: 'enclosing' means: closures only? or classes and files?
|
|
indent = indentsize(lines[lnum])
|
|
block = getblock(lines[lnum:]) #XXX: catch any TokenError here?
|
|
|
|
if not enclosing or not indent:
|
|
if lstrip: block = _outdent(block)
|
|
return ([block], [lnum]) if locate is True else [block]
|
|
|
|
pat1 = r'^(\s*def\s)|(.*(?<!\w)lambda(:|\s))'; pat1 = re.compile(pat1)
|
|
pat2 = r'^(\s*@)'; pat2 = re.compile(pat2)
|
|
#pat3 = r'^(\s*class\s)'; pat3 = re.compile(pat3) #XXX: enclosing class?
|
|
#FIXME: bound methods need enclosing class (and then instantiation)
|
|
# *or* somehow apply a partial using the instance
|
|
|
|
skip = 0
|
|
line = 0
|
|
blocks = []; _lnum = []
|
|
target = ''.join(block)
|
|
while line <= lnum: #XXX: repeat lnum? or until line < lnum?
|
|
# see if starts with ('def','lambda') and contains our target block
|
|
if pat1.match(lines[line]):
|
|
if not skip:
|
|
try: code = getblock(lines[line:])
|
|
except TokenError: code = [lines[line]]
|
|
if indentsize(lines[line]) > indent: #XXX: should be >= ?
|
|
line += len(code) - skip
|
|
elif target in ''.join(code):
|
|
blocks.append(code) # save code block as the potential winner
|
|
_lnum.append(line - skip) # save the line number for the match
|
|
line += len(code) - skip
|
|
else:
|
|
line += 1
|
|
skip = 0
|
|
# find skip: the number of consecutive decorators
|
|
elif pat2.match(lines[line]):
|
|
try: code = getblock(lines[line:])
|
|
except TokenError: code = [lines[line]]
|
|
skip = 1
|
|
for _line in code[1:]: # skip lines that are decorators
|
|
if not pat2.match(_line): break
|
|
skip += 1
|
|
line += skip
|
|
# no match: reset skip and go to the next line
|
|
else:
|
|
line +=1
|
|
skip = 0
|
|
|
|
if not blocks:
|
|
blocks = [block]
|
|
_lnum = [lnum]
|
|
if lstrip: blocks = [_outdent(block) for block in blocks]
|
|
# return last match
|
|
return (blocks, _lnum) if locate is True else blocks
|
|
|
|
|
|
def getsourcelines(object, lstrip=False, enclosing=False):
|
|
"""Return a list of source lines and starting line number for an object.
|
|
Interactively-defined objects refer to lines in the interpreter's history.
|
|
|
|
The argument may be a module, class, method, function, traceback, frame,
|
|
or code object. The source code is returned as a list of the lines
|
|
corresponding to the object and the line number indicates where in the
|
|
original source file the first line of code was found. An IOError is
|
|
raised if the source code cannot be retrieved, while a TypeError is
|
|
raised for objects where the source code is unavailable (e.g. builtins).
|
|
|
|
If lstrip=True, ensure there is no indentation in the first line of code.
|
|
If enclosing=True, then also return any enclosing code."""
|
|
code, n = getblocks(object, lstrip=lstrip, enclosing=enclosing, locate=True)
|
|
return code[-1], n[-1]
|
|
|
|
|
|
#NOTE: broke backward compatibility 4/16/14 (was lstrip=True, force=True)
|
|
def getsource(object, alias='', lstrip=False, enclosing=False, \
|
|
force=False, builtin=False):
|
|
"""Return the text of the source code for an object. The source code for
|
|
interactively-defined objects are extracted from the interpreter's history.
|
|
|
|
The argument may be a module, class, method, function, traceback, frame,
|
|
or code object. The source code is returned as a single string. An
|
|
IOError is raised if the source code cannot be retrieved, while a
|
|
TypeError is raised for objects where the source code is unavailable
|
|
(e.g. builtins).
|
|
|
|
If alias is provided, then add a line of code that renames the object.
|
|
If lstrip=True, ensure there is no indentation in the first line of code.
|
|
If enclosing=True, then also return any enclosing code.
|
|
If force=True, catch (TypeError,IOError) and try to use import hooks.
|
|
If builtin=True, force an import for any builtins
|
|
"""
|
|
# hascode denotes a callable
|
|
hascode = _hascode(object)
|
|
# is a class instance type (and not in builtins)
|
|
instance = _isinstance(object)
|
|
|
|
# get source lines; if fail, try to 'force' an import
|
|
try: # fails for builtins, and other assorted object types
|
|
lines, lnum = getsourcelines(object, enclosing=enclosing)
|
|
except (TypeError, IOError): # failed to get source, resort to import hooks
|
|
if not force: # don't try to get types that findsource can't get
|
|
raise
|
|
if not getmodule(object): # get things like 'None' and '1'
|
|
if not instance: return getimport(object, alias, builtin=builtin)
|
|
# special handling (numpy arrays, ...)
|
|
_import = getimport(object, builtin=builtin)
|
|
name = getname(object, force=True)
|
|
_alias = "%s = " % alias if alias else ""
|
|
if alias == name: _alias = ""
|
|
return _import+_alias+"%s\n" % name
|
|
else: #FIXME: could use a good bit of cleanup, since using getimport...
|
|
if not instance: return getimport(object, alias, builtin=builtin)
|
|
# now we are dealing with an instance...
|
|
name = object.__class__.__name__
|
|
module = object.__module__
|
|
if module in ['builtins','__builtin__']:
|
|
return getimport(object, alias, builtin=builtin)
|
|
else: #FIXME: leverage getimport? use 'from module import name'?
|
|
lines, lnum = ["%s = __import__('%s', fromlist=['%s']).%s\n" % (name,module,name,name)], 0
|
|
obj = eval(lines[0].lstrip(name + ' = '))
|
|
lines, lnum = getsourcelines(obj, enclosing=enclosing)
|
|
|
|
# strip leading indent (helps ensure can be imported)
|
|
if lstrip or alias:
|
|
lines = _outdent(lines)
|
|
|
|
# instantiate, if there's a nice repr #XXX: BAD IDEA???
|
|
if instance: #and force: #XXX: move into findsource or getsourcelines ?
|
|
if '(' in repr(object): lines.append('%r\n' % object)
|
|
#else: #XXX: better to somehow to leverage __reduce__ ?
|
|
# reconstructor,args = object.__reduce__()
|
|
# _ = reconstructor(*args)
|
|
else: # fall back to serialization #XXX: bad idea?
|
|
#XXX: better not duplicate work? #XXX: better new/enclose=True?
|
|
lines = dumpsource(object, alias='', new=force, enclose=False)
|
|
lines, lnum = [line+'\n' for line in lines.split('\n')][:-1], 0
|
|
#else: object.__code__ # raise AttributeError
|
|
|
|
# add an alias to the source code
|
|
if alias:
|
|
if hascode:
|
|
skip = 0
|
|
for line in lines: # skip lines that are decorators
|
|
if not line.startswith('@'): break
|
|
skip += 1
|
|
#XXX: use regex from findsource / getsourcelines ?
|
|
if lines[skip].lstrip().startswith('def '): # we have a function
|
|
if alias != object.__name__:
|
|
lines.append('\n%s = %s\n' % (alias, object.__name__))
|
|
elif 'lambda ' in lines[skip]: # we have a lambda
|
|
if alias != lines[skip].split('=')[0].strip():
|
|
lines[skip] = '%s = %s' % (alias, lines[skip])
|
|
else: # ...try to use the object's name
|
|
if alias != object.__name__:
|
|
lines.append('\n%s = %s\n' % (alias, object.__name__))
|
|
else: # class or class instance
|
|
if instance:
|
|
if alias != lines[-1].split('=')[0].strip():
|
|
lines[-1] = ('%s = ' % alias) + lines[-1]
|
|
else:
|
|
name = getname(object, force=True) or object.__name__
|
|
if alias != name:
|
|
lines.append('\n%s = %s\n' % (alias, name))
|
|
return ''.join(lines)
|
|
|
|
|
|
def _hascode(object):
|
|
'''True if object has an attribute that stores it's __code__'''
|
|
return getattr(object,'__code__',None) or getattr(object,'func_code',None)
|
|
|
|
def _isinstance(object):
|
|
'''True if object is a class instance type (and is not a builtin)'''
|
|
if _hascode(object) or isclass(object) or ismodule(object):
|
|
return False
|
|
if istraceback(object) or isframe(object) or iscode(object):
|
|
return False
|
|
# special handling (numpy arrays, ...)
|
|
if not getmodule(object) and getmodule(type(object)).__name__ in ['numpy']:
|
|
return True
|
|
# # check if is instance of a builtin
|
|
# if not getmodule(object) and getmodule(type(object)).__name__ in ['__builtin__','builtins']:
|
|
# return False
|
|
_types = ('<class ',"<type 'instance'>")
|
|
if not repr(type(object)).startswith(_types): #FIXME: weak hack
|
|
return False
|
|
if not getmodule(object) or object.__module__ in ['builtins','__builtin__'] or getname(object, force=True) in ['array']:
|
|
return False
|
|
return True # by process of elimination... it's what we want
|
|
|
|
|
|
def _intypes(object):
|
|
'''check if object is in the 'types' module'''
|
|
import types
|
|
# allow user to pass in object or object.__name__
|
|
if type(object) is not type(''):
|
|
object = getname(object, force=True)
|
|
if object == 'ellipsis': object = 'EllipsisType'
|
|
return True if hasattr(types, object) else False
|
|
|
|
|
|
def _isstring(object): #XXX: isstringlike better?
|
|
'''check if object is a string-like type'''
|
|
if PY3: return isinstance(object, (str, bytes))
|
|
return isinstance(object, basestring)
|
|
|
|
|
|
def indent(code, spaces=4):
|
|
'''indent a block of code with whitespace (default is 4 spaces)'''
|
|
indent = indentsize(code)
|
|
if type(spaces) is int: spaces = ' '*spaces
|
|
# if '\t' is provided, will indent with a tab
|
|
nspaces = indentsize(spaces)
|
|
# blank lines (etc) need to be ignored
|
|
lines = code.split('\n')
|
|
## stq = "'''"; dtq = '"""'
|
|
## in_stq = in_dtq = False
|
|
for i in range(len(lines)):
|
|
#FIXME: works... but shouldn't indent 2nd+ lines of multiline doc
|
|
_indent = indentsize(lines[i])
|
|
if indent > _indent: continue
|
|
lines[i] = spaces+lines[i]
|
|
## #FIXME: may fail when stq and dtq in same line (depends on ordering)
|
|
## nstq, ndtq = lines[i].count(stq), lines[i].count(dtq)
|
|
## if not in_dtq and not in_stq:
|
|
## lines[i] = spaces+lines[i] # we indent
|
|
## # entering a comment block
|
|
## if nstq%2: in_stq = not in_stq
|
|
## if ndtq%2: in_dtq = not in_dtq
|
|
## # leaving a comment block
|
|
## elif in_dtq and ndtq%2: in_dtq = not in_dtq
|
|
## elif in_stq and nstq%2: in_stq = not in_stq
|
|
## else: pass
|
|
if lines[-1].strip() == '': lines[-1] = ''
|
|
return '\n'.join(lines)
|
|
|
|
|
|
def _outdent(lines, spaces=None, all=True):
|
|
'''outdent lines of code, accounting for docs and line continuations'''
|
|
indent = indentsize(lines[0])
|
|
if spaces is None or spaces > indent or spaces < 0: spaces = indent
|
|
for i in range(len(lines) if all else 1):
|
|
#FIXME: works... but shouldn't outdent 2nd+ lines of multiline doc
|
|
_indent = indentsize(lines[i])
|
|
if spaces > _indent: _spaces = _indent
|
|
else: _spaces = spaces
|
|
lines[i] = lines[i][_spaces:]
|
|
return lines
|
|
|
|
def outdent(code, spaces=None, all=True):
|
|
'''outdent a block of code (default is to strip all leading whitespace)'''
|
|
indent = indentsize(code)
|
|
if spaces is None or spaces > indent or spaces < 0: spaces = indent
|
|
#XXX: will this delete '\n' in some cases?
|
|
if not all: return code[spaces:]
|
|
return '\n'.join(_outdent(code.split('\n'), spaces=spaces, all=all))
|
|
|
|
|
|
#XXX: not sure what the point of _wrap is...
|
|
#exec_ = lambda s, *a: eval(compile(s, '<string>', 'exec'), *a)
|
|
__globals__ = globals()
|
|
__locals__ = locals()
|
|
wrap2 = '''
|
|
def _wrap(f):
|
|
""" encapsulate a function and it's __import__ """
|
|
def func(*args, **kwds):
|
|
try:
|
|
# _ = eval(getsource(f, force=True)) #XXX: safer but less robust
|
|
exec getimportable(f, alias='_') in %s, %s
|
|
except:
|
|
raise ImportError('cannot import name ' + f.__name__)
|
|
return _(*args, **kwds)
|
|
func.__name__ = f.__name__
|
|
func.__doc__ = f.__doc__
|
|
return func
|
|
''' % ('__globals__', '__locals__')
|
|
wrap3 = '''
|
|
def _wrap(f):
|
|
""" encapsulate a function and it's __import__ """
|
|
def func(*args, **kwds):
|
|
try:
|
|
# _ = eval(getsource(f, force=True)) #XXX: safer but less robust
|
|
exec(getimportable(f, alias='_'), %s, %s)
|
|
except:
|
|
raise ImportError('cannot import name ' + f.__name__)
|
|
return _(*args, **kwds)
|
|
func.__name__ = f.__name__
|
|
func.__doc__ = f.__doc__
|
|
return func
|
|
''' % ('__globals__', '__locals__')
|
|
if PY3:
|
|
exec(wrap3)
|
|
else:
|
|
exec(wrap2)
|
|
del wrap2, wrap3
|
|
|
|
|
|
def _enclose(object, alias=''): #FIXME: needs alias to hold returned object
|
|
"""create a function enclosure around the source of some object"""
|
|
#XXX: dummy and stub should append a random string
|
|
dummy = '__this_is_a_big_dummy_enclosing_function__'
|
|
stub = '__this_is_a_stub_variable__'
|
|
code = 'def %s():\n' % dummy
|
|
code += indent(getsource(object, alias=stub, lstrip=True, force=True))
|
|
code += indent('return %s\n' % stub)
|
|
if alias: code += '%s = ' % alias
|
|
code += '%s(); del %s\n' % (dummy, dummy)
|
|
#code += "globals().pop('%s',lambda :None)()\n" % dummy
|
|
return code
|
|
|
|
|
|
def dumpsource(object, alias='', new=False, enclose=True):
|
|
"""'dump to source', where the code includes a pickled object.
|
|
|
|
If new=True and object is a class instance, then create a new
|
|
instance using the unpacked class source code. If enclose, then
|
|
create the object inside a function enclosure (thus minimizing
|
|
any global namespace pollution).
|
|
"""
|
|
from dill import dumps
|
|
pik = repr(dumps(object))
|
|
code = 'import dill\n'
|
|
if enclose:
|
|
stub = '__this_is_a_stub_variable__' #XXX: *must* be same _enclose.stub
|
|
pre = '%s = ' % stub
|
|
new = False #FIXME: new=True doesn't work with enclose=True
|
|
else:
|
|
stub = alias
|
|
pre = '%s = ' % stub if alias else alias
|
|
|
|
# if a 'new' instance is not needed, then just dump and load
|
|
if not new or not _isinstance(object):
|
|
code += pre + 'dill.loads(%s)\n' % pik
|
|
else: #XXX: other cases where source code is needed???
|
|
code += getsource(object.__class__, alias='', lstrip=True, force=True)
|
|
mod = repr(object.__module__) # should have a module (no builtins here)
|
|
if PY3:
|
|
code += pre + 'dill.loads(%s.replace(b%s,bytes(__name__,"UTF-8")))\n' % (pik,mod)
|
|
else:
|
|
code += pre + 'dill.loads(%s.replace(%s,__name__))\n' % (pik,mod)
|
|
#code += 'del %s' % object.__class__.__name__ #NOTE: kills any existing!
|
|
|
|
if enclose:
|
|
# generation of the 'enclosure'
|
|
dummy = '__this_is_a_big_dummy_object__'
|
|
dummy = _enclose(dummy, alias=alias)
|
|
# hack to replace the 'dummy' with the 'real' code
|
|
dummy = dummy.split('\n')
|
|
code = dummy[0]+'\n' + indent(code) + '\n'.join(dummy[-3:])
|
|
|
|
return code #XXX: better 'dumpsourcelines', returning list of lines?
|
|
|
|
|
|
def getname(obj, force=False, fqn=False): #XXX: throw(?) to raise error on fail?
|
|
"""get the name of the object. for lambdas, get the name of the pointer """
|
|
if fqn: return '.'.join(_namespace(obj))
|
|
module = getmodule(obj)
|
|
if not module: # things like "None" and "1"
|
|
if not force: return None
|
|
return repr(obj)
|
|
try:
|
|
#XXX: 'wrong' for decorators and curried functions ?
|
|
# if obj.func_closure: ...use logic from getimportable, etc ?
|
|
name = obj.__name__
|
|
if name == '<lambda>':
|
|
return getsource(obj).split('=',1)[0].strip()
|
|
# handle some special cases
|
|
if module.__name__ in ['builtins','__builtin__']:
|
|
if name == 'ellipsis': name = 'EllipsisType'
|
|
return name
|
|
except AttributeError: #XXX: better to just throw AttributeError ?
|
|
if not force: return None
|
|
name = repr(obj)
|
|
if name.startswith('<'): # or name.split('('):
|
|
return None
|
|
return name
|
|
|
|
|
|
def _namespace(obj):
|
|
"""_namespace(obj); return namespace hierarchy (as a list of names)
|
|
for the given object. For an instance, find the class hierarchy.
|
|
|
|
For example:
|
|
|
|
>>> from functools import partial
|
|
>>> p = partial(int, base=2)
|
|
>>> _namespace(p)
|
|
[\'functools\', \'partial\']
|
|
"""
|
|
# mostly for functions and modules and such
|
|
#FIXME: 'wrong' for decorators and curried functions
|
|
try: #XXX: needs some work and testing on different types
|
|
module = qual = str(getmodule(obj)).split()[1].strip('"').strip("'")
|
|
qual = qual.split('.')
|
|
if ismodule(obj):
|
|
return qual
|
|
# get name of a lambda, function, etc
|
|
name = getname(obj) or obj.__name__ # failing, raise AttributeError
|
|
# check special cases (NoneType, ...)
|
|
if module in ['builtins','__builtin__']: # BuiltinFunctionType
|
|
if _intypes(name): return ['types'] + [name]
|
|
return qual + [name] #XXX: can be wrong for some aliased objects
|
|
except: pass
|
|
# special case: numpy.inf and numpy.nan (we don't want them as floats)
|
|
if str(obj) in ['inf','nan','Inf','NaN']: # is more, but are they needed?
|
|
return ['numpy'] + [str(obj)]
|
|
# mostly for classes and class instances and such
|
|
module = getattr(obj.__class__, '__module__', None)
|
|
qual = str(obj.__class__)
|
|
try: qual = qual[qual.index("'")+1:-2]
|
|
except ValueError: pass # str(obj.__class__) made the 'try' unnecessary
|
|
qual = qual.split(".")
|
|
if module in ['builtins','__builtin__']:
|
|
# check special cases (NoneType, Ellipsis, ...)
|
|
if qual[-1] == 'ellipsis': qual[-1] = 'EllipsisType'
|
|
if _intypes(qual[-1]): module = 'types' #XXX: BuiltinFunctionType
|
|
qual = [module] + qual
|
|
return qual
|
|
|
|
|
|
#NOTE: 05/25/14 broke backward compatability: added 'alias' as 3rd argument
|
|
def _getimport(head, tail, alias='', verify=True, builtin=False):
|
|
"""helper to build a likely import string from head and tail of namespace.
|
|
('head','tail') are used in the following context: "from head import tail"
|
|
|
|
If verify=True, then test the import string before returning it.
|
|
If builtin=True, then force an import for builtins where possible.
|
|
If alias is provided, then rename the object on import.
|
|
"""
|
|
# special handling for a few common types
|
|
if tail in ['Ellipsis', 'NotImplemented'] and head in ['types']:
|
|
head = len.__module__
|
|
elif tail in ['None'] and head in ['types']:
|
|
_alias = '%s = ' % alias if alias else ''
|
|
if alias == tail: _alias = ''
|
|
return _alias+'%s\n' % tail
|
|
# we don't need to import from builtins, so return ''
|
|
# elif tail in ['NoneType','int','float','long','complex']: return '' #XXX: ?
|
|
if head in ['builtins','__builtin__']:
|
|
# special cases (NoneType, Ellipsis, ...) #XXX: BuiltinFunctionType
|
|
if tail == 'ellipsis': tail = 'EllipsisType'
|
|
if _intypes(tail): head = 'types'
|
|
elif not builtin:
|
|
_alias = '%s = ' % alias if alias else ''
|
|
if alias == tail: _alias = ''
|
|
return _alias+'%s\n' % tail
|
|
else: pass # handle builtins below
|
|
# get likely import string
|
|
if not head: _str = "import %s" % tail
|
|
else: _str = "from %s import %s" % (head, tail)
|
|
_alias = " as %s\n" % alias if alias else "\n"
|
|
if alias == tail: _alias = "\n"
|
|
_str += _alias
|
|
# FIXME: fails on most decorators, currying, and such...
|
|
# (could look for magic __wrapped__ or __func__ attr)
|
|
# (could fix in 'namespace' to check obj for closure)
|
|
if verify and not head.startswith('dill.'):# weird behavior for dill
|
|
#print(_str)
|
|
try: exec(_str) #XXX: check if == obj? (name collision)
|
|
except ImportError: #XXX: better top-down or bottom-up recursion?
|
|
_head = head.rsplit(".",1)[0] #(or get all, then compare == obj?)
|
|
if not _head: raise
|
|
if _head != head:
|
|
_str = _getimport(_head, tail, alias, verify)
|
|
return _str
|
|
|
|
|
|
#XXX: rename builtin to force? vice versa? verify to force? (as in getsource)
|
|
#NOTE: 05/25/14 broke backward compatability: added 'alias' as 2nd argument
|
|
def getimport(obj, alias='', verify=True, builtin=False, enclosing=False):
|
|
"""get the likely import string for the given object
|
|
|
|
obj is the object to inspect
|
|
If verify=True, then test the import string before returning it.
|
|
If builtin=True, then force an import for builtins where possible.
|
|
If enclosing=True, get the import for the outermost enclosing callable.
|
|
If alias is provided, then rename the object on import.
|
|
"""
|
|
if enclosing:
|
|
from .detect import outermost
|
|
_obj = outermost(obj)
|
|
obj = _obj if _obj else obj
|
|
# get the namespace
|
|
qual = _namespace(obj)
|
|
head = '.'.join(qual[:-1])
|
|
tail = qual[-1]
|
|
# for named things... with a nice repr #XXX: move into _namespace?
|
|
try: # look for '<...>' and be mindful it might be in lists, dicts, etc...
|
|
name = repr(obj).split('<',1)[1].split('>',1)[1]
|
|
name = None # we have a 'object'-style repr
|
|
except: # it's probably something 'importable'
|
|
if head in ['builtins','__builtin__']:
|
|
name = repr(obj) #XXX: catch [1,2], (1,2), set([1,2])... others?
|
|
else:
|
|
name = repr(obj).split('(')[0]
|
|
#if not repr(obj).startswith('<'): name = repr(obj).split('(')[0]
|
|
#else: name = None
|
|
if name: # try using name instead of tail
|
|
try: return _getimport(head, name, alias, verify, builtin)
|
|
except ImportError: pass
|
|
except SyntaxError:
|
|
if head in ['builtins','__builtin__']:
|
|
_alias = '%s = ' % alias if alias else ''
|
|
if alias == name: _alias = ''
|
|
return _alias+'%s\n' % name
|
|
else: pass
|
|
try:
|
|
#if type(obj) is type(abs): _builtin = builtin # BuiltinFunctionType
|
|
#else: _builtin = False
|
|
return _getimport(head, tail, alias, verify, builtin)
|
|
except ImportError:
|
|
raise # could do some checking against obj
|
|
except SyntaxError:
|
|
if head in ['builtins','__builtin__']:
|
|
_alias = '%s = ' % alias if alias else ''
|
|
if alias == tail: _alias = ''
|
|
return _alias+'%s\n' % tail
|
|
raise # could do some checking against obj
|
|
|
|
|
|
def _importable(obj, alias='', source=None, enclosing=False, force=True, \
|
|
builtin=True, lstrip=True):
|
|
"""get an import string (or the source code) for the given object
|
|
|
|
This function will attempt to discover the name of the object, or the repr
|
|
of the object, or the source code for the object. To attempt to force
|
|
discovery of the source code, use source=True, to attempt to force the
|
|
use of an import, use source=False; otherwise an import will be sought
|
|
for objects not defined in __main__. The intent is to build a string
|
|
that can be imported from a python file. obj is the object to inspect.
|
|
If alias is provided, then rename the object with the given alias.
|
|
|
|
If source=True, use these options:
|
|
If enclosing=True, then also return any enclosing code.
|
|
If force=True, catch (TypeError,IOError) and try to use import hooks.
|
|
If lstrip=True, ensure there is no indentation in the first line of code.
|
|
|
|
If source=False, use these options:
|
|
If enclosing=True, get the import for the outermost enclosing callable.
|
|
If force=True, then don't test the import string before returning it.
|
|
If builtin=True, then force an import for builtins where possible.
|
|
"""
|
|
if source is None:
|
|
source = True if isfrommain(obj) else False
|
|
if source: # first try to get the source
|
|
try:
|
|
return getsource(obj, alias, enclosing=enclosing, \
|
|
force=force, lstrip=lstrip, builtin=builtin)
|
|
except: pass
|
|
try:
|
|
if not _isinstance(obj):
|
|
return getimport(obj, alias, enclosing=enclosing, \
|
|
verify=(not force), builtin=builtin)
|
|
# first 'get the import', then 'get the instance'
|
|
_import = getimport(obj, enclosing=enclosing, \
|
|
verify=(not force), builtin=builtin)
|
|
name = getname(obj, force=True)
|
|
if not name:
|
|
raise AttributeError("object has no atribute '__name__'")
|
|
_alias = "%s = " % alias if alias else ""
|
|
if alias == name: _alias = ""
|
|
return _import+_alias+"%s\n" % name
|
|
|
|
except: pass
|
|
if not source: # try getsource, only if it hasn't been tried yet
|
|
try:
|
|
return getsource(obj, alias, enclosing=enclosing, \
|
|
force=force, lstrip=lstrip, builtin=builtin)
|
|
except: pass
|
|
# get the name (of functions, lambdas, and classes)
|
|
# or hope that obj can be built from the __repr__
|
|
#XXX: what to do about class instances and such?
|
|
obj = getname(obj, force=force)
|
|
# we either have __repr__ or __name__ (or None)
|
|
if not obj or obj.startswith('<'):
|
|
raise AttributeError("object has no atribute '__name__'")
|
|
_alias = '%s = ' % alias if alias else ''
|
|
if alias == obj: _alias = ''
|
|
return _alias+'%s\n' % obj
|
|
#XXX: possible failsafe... (for example, for instances when source=False)
|
|
# "import dill; result = dill.loads(<pickled_object>); # repr(<object>)"
|
|
|
|
def _closuredimport(func, alias='', builtin=False):
|
|
"""get import for closured objects; return a dict of 'name' and 'import'"""
|
|
import re
|
|
from .detect import freevars, outermost
|
|
free_vars = freevars(func)
|
|
func_vars = {}
|
|
# split into 'funcs' and 'non-funcs'
|
|
for name,obj in list(free_vars.items()):
|
|
if not isfunction(obj): continue
|
|
# get import for 'funcs'
|
|
fobj = free_vars.pop(name)
|
|
src = getsource(fobj)
|
|
if src.lstrip().startswith('@'): # we have a decorator
|
|
src = getimport(fobj, alias=alias, builtin=builtin)
|
|
else: # we have to "hack" a bit... and maybe be lucky
|
|
encl = outermost(func)
|
|
# pattern: 'func = enclosing(fobj'
|
|
pat = '.*[\w\s]=\s*'+getname(encl)+'\('+getname(fobj)
|
|
mod = getname(getmodule(encl))
|
|
#HACK: get file containing 'outer' function; is func there?
|
|
lines,_ = findsource(encl)
|
|
candidate = [line for line in lines if getname(encl) in line and \
|
|
re.match(pat, line)]
|
|
if not candidate:
|
|
mod = getname(getmodule(fobj))
|
|
#HACK: get file containing 'inner' function; is func there?
|
|
lines,_ = findsource(fobj)
|
|
candidate = [line for line in lines \
|
|
if getname(fobj) in line and re.match(pat, line)]
|
|
if not len(candidate): raise TypeError('import could not be found')
|
|
candidate = candidate[-1]
|
|
name = candidate.split('=',1)[0].split()[-1].strip()
|
|
src = _getimport(mod, name, alias=alias, builtin=builtin)
|
|
func_vars[name] = src
|
|
if not func_vars:
|
|
name = outermost(func)
|
|
mod = getname(getmodule(name))
|
|
if not mod or name is func: # then it can be handled by getimport
|
|
name = getname(func, force=True) #XXX: better key?
|
|
src = getimport(func, alias=alias, builtin=builtin)
|
|
else:
|
|
lines,_ = findsource(name)
|
|
# pattern: 'func = enclosing('
|
|
candidate = [line for line in lines if getname(name) in line and \
|
|
re.match('.*[\w\s]=\s*'+getname(name)+'\(', line)]
|
|
if not len(candidate): raise TypeError('import could not be found')
|
|
candidate = candidate[-1]
|
|
name = candidate.split('=',1)[0].split()[-1].strip()
|
|
src = _getimport(mod, name, alias=alias, builtin=builtin)
|
|
func_vars[name] = src
|
|
return func_vars
|
|
|
|
#XXX: should be able to use __qualname__
|
|
def _closuredsource(func, alias=''):
|
|
"""get source code for closured objects; return a dict of 'name'
|
|
and 'code blocks'"""
|
|
#FIXME: this entire function is a messy messy HACK
|
|
# - pollutes global namespace
|
|
# - fails if name of freevars are reused
|
|
# - can unnecessarily duplicate function code
|
|
from .detect import freevars
|
|
free_vars = freevars(func)
|
|
func_vars = {}
|
|
# split into 'funcs' and 'non-funcs'
|
|
for name,obj in list(free_vars.items()):
|
|
if not isfunction(obj):
|
|
# get source for 'non-funcs'
|
|
free_vars[name] = getsource(obj, force=True, alias=name)
|
|
continue
|
|
# get source for 'funcs'
|
|
fobj = free_vars.pop(name)
|
|
src = getsource(fobj, alias) # DO NOT include dependencies
|
|
# if source doesn't start with '@', use name as the alias
|
|
if not src.lstrip().startswith('@'): #FIXME: 'enclose' in dummy;
|
|
src = importable(fobj,alias=name)# wrong ref 'name'
|
|
org = getsource(func, alias, enclosing=False, lstrip=True)
|
|
src = (src, org) # undecorated first, then target
|
|
else: #NOTE: reproduces the code!
|
|
org = getsource(func, enclosing=True, lstrip=False)
|
|
src = importable(fobj, alias, source=True) # include dependencies
|
|
src = (org, src) # target first, then decorated
|
|
func_vars[name] = src
|
|
src = ''.join(free_vars.values())
|
|
if not func_vars: #FIXME: 'enclose' in dummy; wrong ref 'name'
|
|
org = getsource(func, alias, force=True, enclosing=False, lstrip=True)
|
|
src = (src, org) # variables first, then target
|
|
else:
|
|
src = (src, None) # just variables (better '' instead of None?)
|
|
func_vars[None] = src
|
|
# FIXME: remove duplicates (however, order is important...)
|
|
return func_vars
|
|
|
|
def importable(obj, alias='', source=None, builtin=True):
|
|
"""get an importable string (i.e. source code or the import string)
|
|
for the given object, including any required objects from the enclosing
|
|
and global scope
|
|
|
|
This function will attempt to discover the name of the object, or the repr
|
|
of the object, or the source code for the object. To attempt to force
|
|
discovery of the source code, use source=True, to attempt to force the
|
|
use of an import, use source=False; otherwise an import will be sought
|
|
for objects not defined in __main__. The intent is to build a string
|
|
that can be imported from a python file.
|
|
|
|
obj is the object to inspect. If alias is provided, then rename the
|
|
object with the given alias. If builtin=True, then force an import for
|
|
builtins where possible.
|
|
"""
|
|
#NOTE: we always 'force', and 'lstrip' as necessary
|
|
#NOTE: for 'enclosing', use importable(outermost(obj))
|
|
if source is None:
|
|
source = True if isfrommain(obj) else False
|
|
elif builtin and isbuiltin(obj):
|
|
source = False
|
|
tried_source = tried_import = False
|
|
while True:
|
|
if not source: # we want an import
|
|
try:
|
|
if _isinstance(obj): # for instances, punt to _importable
|
|
return _importable(obj, alias, source=False, builtin=builtin)
|
|
src = _closuredimport(obj, alias=alias, builtin=builtin)
|
|
if len(src) == 0:
|
|
raise NotImplementedError('not implemented')
|
|
if len(src) > 1:
|
|
raise NotImplementedError('not implemented')
|
|
return list(src.values())[0]
|
|
except:
|
|
if tried_source: raise
|
|
tried_import = True
|
|
# we want the source
|
|
try:
|
|
src = _closuredsource(obj, alias=alias)
|
|
if len(src) == 0:
|
|
raise NotImplementedError('not implemented')
|
|
# groan... an inline code stitcher
|
|
def _code_stitcher(block):
|
|
"stitch together the strings in tuple 'block'"
|
|
if block[0] and block[-1]: block = '\n'.join(block)
|
|
elif block[0]: block = block[0]
|
|
elif block[-1]: block = block[-1]
|
|
else: block = ''
|
|
return block
|
|
# get free_vars first
|
|
_src = _code_stitcher(src.pop(None))
|
|
_src = [_src] if _src else []
|
|
# get func_vars
|
|
for xxx in src.values():
|
|
xxx = _code_stitcher(xxx)
|
|
if xxx: _src.append(xxx)
|
|
# make a single source string
|
|
if not len(_src):
|
|
src = ''
|
|
elif len(_src) == 1:
|
|
src = _src[0]
|
|
else:
|
|
src = '\n'.join(_src)
|
|
# get source code of objects referred to by obj in global scope
|
|
from .detect import globalvars
|
|
obj = globalvars(obj) #XXX: don't worry about alias? recurse? etc?
|
|
obj = list(getsource(_obj,name,force=True) for (name,_obj) in obj.items() if not isbuiltin(_obj))
|
|
obj = '\n'.join(obj) if obj else ''
|
|
# combine all referred-to source (global then enclosing)
|
|
if not obj: return src
|
|
if not src: return obj
|
|
return obj + src
|
|
except:
|
|
if tried_import: raise
|
|
tried_source = True
|
|
source = not source
|
|
# should never get here
|
|
return
|
|
|
|
|
|
# backward compatability
|
|
def getimportable(obj, alias='', byname=True, explicit=False):
|
|
return importable(obj,alias,source=(not byname),builtin=explicit)
|
|
#return outdent(_importable(obj,alias,source=(not byname),builtin=explicit))
|
|
def likely_import(obj, passive=False, explicit=False):
|
|
return getimport(obj, verify=(not passive), builtin=explicit)
|
|
def _likely_import(first, last, passive=False, explicit=True):
|
|
return _getimport(first, last, verify=(not passive), builtin=explicit)
|
|
_get_name = getname
|
|
getblocks_from_history = getblocks
|
|
|
|
|
|
|
|
# EOF
|