Don't like ads? PRO users don't see any ads ;-)
Guest

Untitled

By: a guest on May 24th, 2012  |  syntax: None  |  size: 8.31 KB  |  hits: 13  |  expires: Never
download  |  raw  |  embed  |  report abuse  |  print
Text below is selected. Please press Ctrl+C to copy to your clipboard. (⌘+C on Mac)
  1. import inspect
  2. import code
  3. import re
  4. import editor
  5. import tokenize
  6. import linecache
  7. import shutil
  8. import time
  9. import functools
  10.  
  11.  
  12. global tweak_history, tweak_originals
  13. tweak_history = {} # Stores a list of versions of the source code of a function
  14. tweak_originals = {} # Stores a
  15.  
  16. #Only wrapped in a class so that I can just use T() instead of T.tweak() while still having access to T.apply() etc.
  17.  
  18. class Tweaker(object):
  19.     @staticmethod
  20.     def __call__(ob):
  21.         """Same as tweak(ob)"""
  22.         Tweaker.tweak(ob)
  23.    
  24.     @staticmethod
  25.     def tweak(ob):
  26.         """
  27.             Edit the source code of a function using an external
  28.             editor, and then - without reloading - use the new
  29.             function definition in your current code.
  30.  
  31.             If you liked the tweaks you made, you can permenantly
  32.             apply() them, or if they broke stuff, revert() them.
  33.        
  34.             Methods of instances use the same function as their
  35.             class, so changing the method of one instance changes
  36.             them ALL.
  37.  
  38.             The function is looked up from this method's argument
  39.             using getfunction() so see notes there for what to pass.
  40.  
  41.             Be careful while editing, do not play around with the
  42.             indentation too much, you are liable to break things.
  43.         """
  44.  
  45.         olds = getsource(ob)
  46.         news = editor.edit(olds, filesuffix=".py")
  47.         if news:
  48.             setsource(ob, news)
  49.         else:
  50.             print "Nothing changed"
  51.  
  52.     @staticmethod
  53.     def getfunction(orig):
  54.         """
  55.             Get the actual function from the argument - tries the following:
  56.  
  57.             If we are passed a string, eval it in all the stack frames above us until it works
  58.             If we have a partial, get it's .func property
  59.             If we have a method, get it's im_func property
  60.             If we have a function that looks like a decorator.decorator decorated one, get the original function
  61.             If we have a function with a name that doesn't match the string input's name, try and jump out of a closure
  62.         """
  63.         output = orig
  64.         name = None
  65.  
  66.         #If we are passed a string, first try and find what it refers to,
  67.         # We will use the name to attempt to jump out of a decorator closure.
  68.         if type(orig) is str:
  69.             for stackframe in [x[0] for x in inspect.stack()[2:]]:
  70.                 try:
  71.                     output = eval(orig, stackframe.f_locals, stackframe.f_globals)
  72.                     try:
  73.                         name = orig[orig.rindex(".")+1:]
  74.                     except:
  75.                         name = orig
  76.                     break
  77.                 except:
  78.                     pass
  79.  
  80.         #Get the original function that a partial refers to
  81.         if type(output) is functools.partial:
  82.             output = output.func
  83.  
  84.         #Get the actual function a method refers to
  85.         if inspect.ismethod(orig):
  86.             output = output.im_func
  87.        
  88.         #This is the only way I've found to identify functions decorated by decorator.decorator
  89.         if inspect.isfunction(output) and '_func_' in output.func_globals:
  90.             output = output.func_globals['_func_']
  91.  
  92.         #What about callables masquerading as functions?
  93.         if callable(output) and not inspect.isfunction(output):
  94.             output = output.__call__
  95.  
  96.         #Now we try and guess our way out of a normal decorator closure
  97.         if name and inspect.isfunction(output) and output.__name__ != name:
  98.             for idx in range(len(output.func_closure)):
  99.                 poss = output.func_closure[idx].cell_contents
  100.                 if inspect.isfunction(poss) and poss.__name__ == name:
  101.                     output = poss
  102.  
  103.         if not inspect.isfunction(output):
  104.             raise Exception("You can only tweak functions.")
  105.        
  106.         return output
  107.  
  108.     @staticmethod
  109.     def getsource(ob):
  110.         """
  111.             Get's the source code of a function. If you aren't sure that you
  112.             have the most basic bit of the function, use getsource() to find it
  113.             before calling this.
  114.         """
  115.         global tweak_history, tweak_originals
  116.         name = None
  117.  
  118.         fun = Tweaker.getfunction(ob)
  119.         if not fun in tweak_history:
  120.  
  121.             #        try:
  122.                 sourcefile = inspect.getsourcefile(fun)
  123.                 lines, start = inspect.findsource(fun)
  124.  
  125.                 blockfinder = inspect.BlockFinder()
  126.                 try:
  127.                     tokenize.tokenize(iter(lines[start:]).next, blockfinder.tokeneater)
  128.                 except (inspect.EndOfBlock, IndentationError):
  129.                     pass
  130.                 end = blockfinder.last
  131.  
  132.                 tweak_originals[fun] = (sourcefile, start, start+end)
  133.                 tweak_history[fun] = ["".join(lines[start:start+end])]
  134.             #except:
  135.             #    raise Exception("Could not find source, probably an interpreter-defined function?")
  136.  
  137.         return tweak_history[fun][-1]
  138.    
  139.     @staticmethod
  140.     def setsource(ob, src):
  141.         """
  142.             Redefine a function using the new definition given.
  143.             src must be the complete function body including the
  144.             def funcname(....):
  145.                 a = b
  146.                 ...
  147.            
  148.             Will fail unless getsource(fun) has been done before
  149.             This is to prevent edit-conflicts on apply.
  150.         """
  151.         global tweak_history
  152.         fun = Tweaker.getfunction(ob)
  153.         try:
  154.             tweak_history[fun].append(src)
  155.         except:
  156.             raise Exception("Cannot setsource() without getsource()")
  157.        
  158.         #Find the function definition, probably ok to get fun.__name__, but possible for user to change name in tweak
  159.         #Can't use .match() as there may be decorators
  160.         m = re.search(r'\s*def\s+(\w+)\s*\(',src)
  161.         if m:
  162.             funname = m.group(1)
  163.             interpreter = code.InteractiveConsole(fun.func_globals)
  164.            
  165.             #Strip whitespace from decorators and def
  166.             lines = src.split("\n")
  167.            
  168.             for i in range(len(lines)):
  169.                 lines[i] = lines[i].lstrip()
  170.                 if lines[i].startswith("def"):
  171.                     break
  172.  
  173.             src = "\n".join(lines)
  174.  
  175.             if not interpreter.push(src):
  176.                 newobj = interpreter.locals[funname]
  177.                 if inspect.ismethoddescriptor(newobj):
  178.                     newobj = newobj.__get__(1) #I have no idea wht significance this paramter has.
  179.                 for attr in dir(newobj):
  180.                     if attr not in ['func_closure', 'func_globals', '__class__']:
  181.                         setattr(fun, attr, getattr(newobj, attr))
  182.                 return
  183.             else:
  184.                 raise Exception("True?")
  185.  
  186.         raise Exception("Could not setsource() probably invalid syntax.")
  187.  
  188.     @staticmethod
  189.     def apply(ob,filename=None):
  190.         """
  191.             Saves the changes you have made during this lot of tweaking to the file.
  192.             TODO allow putting modules/classes with modified functions here.
  193.  
  194.             By default the filename will be that which was read from, but in some cases
  195.             it may be useful to override that.
  196.         """
  197.         global tweak_originals
  198.         global tweak_history
  199.         fun = Tweaker.getfunction(ob)
  200.  
  201.         try:
  202.             (sourcefile, start, end) = tweak_originals[fun]
  203.             if filename:
  204.                 sourcefile = filename
  205.         except:
  206.             return #Not tweaked, so already applied
  207.        
  208.         lines = linecache.getlines(sourcefile) #The file has already been read into python, so no need to read it again
  209.        
  210.         if not "".join(lines[start:end]) == tweak_history[fun][0]:
  211.             raise Exception("Source file has changed on disk")
  212.  
  213.         shutil.copy(sourcefile, sourcefile + ".bak" + str(int(time.time())) )
  214.         file = open(sourcefile, "w")
  215.         file.write("".join(lines[:start]) + tweak_history[fun][-1] + "".join(lines[end:]))
  216.         file.close()
  217.         print "Written new version"
  218.  
  219.     @staticmethod
  220.     def revert(ob, steps=1):
  221.         global tweak_history
  222.         fun = Tweaker.getfunction(ob)
  223.         if fun in tweak_history:
  224.             setsource(fun, tweak_history[fun][0-steps])
  225.         else:
  226.             raise Exception("%s has not been tweaked")
  227.  
  228.  
  229. T = Tweaker()
  230. tweak = T.tweak
  231. apply = T.apply
  232. revert = T.revert
  233. getsource = T.getsource
  234. setsource = T.setsource
  235. getfunction = T.getfunction