Tuesday, 3 April 2007

Refactoring Function Names in Python

I read an interesting article yesterday about how automating name refactoring in dynamically written languages can be done: http://dogbiscuit.org/mdub/weblog/Tech/Programming/Ruby/RubyMethodRenamed The idea is that you rename your function, and then create a method with the original name that will redirect to the new function and in addition track down the file it was called from, and rename the function call in that file. Thought I'd give it a go in Python, and so created a decorator for the job. Here it is in a basic form sans exception handling and logging.
import inspect, os, re, os.path

class renameto(object): 
 def __init__(self, new_function):
     self.new_function = new_function

 def __call__(self, function):
     self.function = function
     return self.decorator
  
 def decorator(self, *args, **kw):
     f = inspect.currentframe().f_back
     fn = f.f_code.co_filename
     lineno = f.f_lineno
  
     in_f = file(fn)
     out_l = []
  
     for i, line in enumerate(in_f):
         if i == (lineno - 1):
             line = re.sub(r"\b%s\b" % self.function.__name__,
                                         self.new_function.__name__, line)
         out_l.append(line)
  
     in_f.close()
  
     temp = fn + "~"
  
     if os.path.exists(temp):
         os.remove(temp)
      
     os.rename(fn, temp)
  
     out_f = file(fn, "w")
  
     for line in out_l:
         out_f.write(line)
     out_f.close()
  
     return self.new_function(*args, **kw)


def newf(arg):
 print "New trace Function: " + arg

@renameto(newf)
def bob(arg):
 pass
Note that what needs to be done to refactor the name is to copy the old method to the new method name, and then decorate the old method with the @renameto(newname) decorator. Optionally the body of the old method can be removed (or an exception thrown - it should never actually get called). Now run all of your tests - the refactoring should be automatically done (save for perhaps a handful of cases). This of course won't work in it's current form with functions that are assigned to a different variable. For example:
disguised_name = bob
disguised_name("Bet this one can't be changed!")
So some more work is required here to handle odd cases like this. Interesting idea though...

No comments: