Created
October 15, 2011 01:58
-
-
Save ConradIrwin/1288882 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import inspect | |
| import code | |
| import re | |
| import editor | |
| import tokenize | |
| import linecache | |
| import shutil | |
| import time | |
| import functools | |
| global tweak_history, tweak_originals | |
| tweak_history = {} # Stores a list of versions of the source code of a function | |
| tweak_originals = {} # Stores a | |
| #Only wrapped in a class so that I can just use T() instead of T.tweak() while still having access to T.apply() etc. | |
| class Tweaker(object): | |
| @staticmethod | |
| def __call__(ob): | |
| """Same as tweak(ob)""" | |
| Tweaker.tweak(ob) | |
| @staticmethod | |
| def tweak(ob): | |
| """ | |
| Edit the source code of a function using an external | |
| editor, and then - without reloading - use the new | |
| function definition in your current code. | |
| If you liked the tweaks you made, you can permenantly | |
| apply() them, or if they broke stuff, revert() them. | |
| Methods of instances use the same function as their | |
| class, so changing the method of one instance changes | |
| them ALL. | |
| The function is looked up from this method's argument | |
| using getfunction() so see notes there for what to pass. | |
| Be careful while editing, do not play around with the | |
| indentation too much, you are liable to break things. | |
| """ | |
| olds = getsource(ob) | |
| news = editor.edit(olds, filesuffix=".py") | |
| if news: | |
| setsource(ob, news) | |
| else: | |
| print "Nothing changed" | |
| @staticmethod | |
| def getfunction(orig): | |
| """ | |
| Get the actual function from the argument - tries the following: | |
| If we are passed a string, eval it in all the stack frames above us until it works | |
| If we have a partial, get it's .func property | |
| If we have a method, get it's im_func property | |
| If we have a function that looks like a decorator.decorator decorated one, get the original function | |
| If we have a function with a name that doesn't match the string input's name, try and jump out of a closure | |
| """ | |
| output = orig | |
| name = None | |
| #If we are passed a string, first try and find what it refers to, | |
| # We will use the name to attempt to jump out of a decorator closure. | |
| if type(orig) is str: | |
| for stackframe in [x[0] for x in inspect.stack()[2:]]: | |
| try: | |
| output = eval(orig, stackframe.f_locals, stackframe.f_globals) | |
| try: | |
| name = orig[orig.rindex(".")+1:] | |
| except: | |
| name = orig | |
| break | |
| except: | |
| pass | |
| #Get the original function that a partial refers to | |
| if type(output) is functools.partial: | |
| output = output.func | |
| #Get the actual function a method refers to | |
| if inspect.ismethod(orig): | |
| output = output.im_func | |
| #This is the only way I've found to identify functions decorated by decorator.decorator | |
| if inspect.isfunction(output) and '_func_' in output.func_globals: | |
| output = output.func_globals['_func_'] | |
| #What about callables masquerading as functions? | |
| if callable(output) and not inspect.isfunction(output): | |
| output = output.__call__ | |
| #Now we try and guess our way out of a normal decorator closure | |
| if name and inspect.isfunction(output) and output.__name__ != name: | |
| for idx in range(len(output.func_closure)): | |
| poss = output.func_closure[idx].cell_contents | |
| if inspect.isfunction(poss) and poss.__name__ == name: | |
| output = poss | |
| if not inspect.isfunction(output): | |
| raise Exception("You can only tweak functions.") | |
| return output | |
| @staticmethod | |
| def getsource(ob): | |
| """ | |
| Get's the source code of a function. If you aren't sure that you | |
| have the most basic bit of the function, use getsource() to find it | |
| before calling this. | |
| """ | |
| global tweak_history, tweak_originals | |
| name = None | |
| fun = Tweaker.getfunction(ob) | |
| if not fun in tweak_history: | |
| # try: | |
| sourcefile = inspect.getsourcefile(fun) | |
| lines, start = inspect.findsource(fun) | |
| blockfinder = inspect.BlockFinder() | |
| try: | |
| tokenize.tokenize(iter(lines[start:]).next, blockfinder.tokeneater) | |
| except (inspect.EndOfBlock, IndentationError): | |
| pass | |
| end = blockfinder.last | |
| tweak_originals[fun] = (sourcefile, start, start+end) | |
| tweak_history[fun] = ["".join(lines[start:start+end])] | |
| #except: | |
| # raise Exception("Could not find source, probably an interpreter-defined function?") | |
| return tweak_history[fun][-1] | |
| @staticmethod | |
| def setsource(ob, src): | |
| """ | |
| Redefine a function using the new definition given. | |
| src must be the complete function body including the | |
| def funcname(....): | |
| a = b | |
| ... | |
| Will fail unless getsource(fun) has been done before | |
| This is to prevent edit-conflicts on apply. | |
| """ | |
| global tweak_history | |
| fun = Tweaker.getfunction(ob) | |
| try: | |
| tweak_history[fun].append(src) | |
| except: | |
| raise Exception("Cannot setsource() without getsource()") | |
| #Find the function definition, probably ok to get fun.__name__, but possible for user to change name in tweak | |
| #Can't use .match() as there may be decorators | |
| m = re.search(r'\s*def\s+(\w+)\s*\(',src) | |
| if m: | |
| funname = m.group(1) | |
| interpreter = code.InteractiveConsole(fun.func_globals) | |
| #Strip whitespace from decorators and def | |
| lines = src.split("\n") | |
| for i in range(len(lines)): | |
| lines[i] = lines[i].lstrip() | |
| if lines[i].startswith("def"): | |
| break | |
| src = "\n".join(lines) | |
| if not interpreter.push(src): | |
| newobj = interpreter.locals[funname] | |
| if inspect.ismethoddescriptor(newobj): | |
| newobj = newobj.__get__(1) #I have no idea wht significance this paramter has. | |
| for attr in dir(newobj): | |
| if attr not in ['func_closure', 'func_globals', '__class__']: | |
| setattr(fun, attr, getattr(newobj, attr)) | |
| return | |
| else: | |
| raise Exception("True?") | |
| raise Exception("Could not setsource() probably invalid syntax.") | |
| @staticmethod | |
| def apply(ob,filename=None): | |
| """ | |
| Saves the changes you have made during this lot of tweaking to the file. | |
| TODO allow putting modules/classes with modified functions here. | |
| By default the filename will be that which was read from, but in some cases | |
| it may be useful to override that. | |
| """ | |
| global tweak_originals | |
| global tweak_history | |
| fun = Tweaker.getfunction(ob) | |
| try: | |
| (sourcefile, start, end) = tweak_originals[fun] | |
| if filename: | |
| sourcefile = filename | |
| except: | |
| return #Not tweaked, so already applied | |
| lines = linecache.getlines(sourcefile) #The file has already been read into python, so no need to read it again | |
| if not "".join(lines[start:end]) == tweak_history[fun][0]: | |
| raise Exception("Source file has changed on disk") | |
| shutil.copy(sourcefile, sourcefile + ".bak" + str(int(time.time())) ) | |
| file = open(sourcefile, "w") | |
| file.write("".join(lines[:start]) + tweak_history[fun][-1] + "".join(lines[end:])) | |
| file.close() | |
| print "Written new version" | |
| @staticmethod | |
| def revert(ob, steps=1): | |
| global tweak_history | |
| fun = Tweaker.getfunction(ob) | |
| if fun in tweak_history: | |
| setsource(fun, tweak_history[fun][0-steps]) | |
| else: | |
| raise Exception("%s has not been tweaked") | |
| T = Tweaker() | |
| tweak = T.tweak | |
| apply = T.apply | |
| revert = T.revert | |
| getsource = T.getsource | |
| setsource = T.setsource | |
| getfunction = T.getfunction |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment