Skip to content

Instantly share code, notes, and snippets.

@ConradIrwin
Created October 15, 2011 01:58
Show Gist options
  • Select an option

  • Save ConradIrwin/1288882 to your computer and use it in GitHub Desktop.

Select an option

Save ConradIrwin/1288882 to your computer and use it in GitHub Desktop.
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