python metaprogramming

Recently, I’ve been using python a good bit at work and have really started to enjoy the language. The meta-programming strengths of python drastically increases what you can do with very little code. I’m really enjoying learning the language.

Today, I wrote the following code today mainly to get more familiar with the ‘magic’ methods of python.

There’s a method in the module difflib called ‘get_close_matches‘ that does approximate string matching. I used this find any close matches to any misspelled words. Another module that I have come to really enjoy in python is the inspect module. This allows you to find methods, classes etc. during runtime.

Two more modules that I haven’t really used before are logging and traceback. Logging has some neat functionality for customizing logging information, but I won’t show that here. I’ve never really had access to the information that the traceback module provides, giving the programmer complete access to the error stack, file, line and error in a very easy to use api.

Using these libraries, combined with python’s inherent strengths, I wrote some code to catch misspelled variables and functions. It might not be very useful in a production environment, but I find it neat none the less.

import difflib, traceback, logging, inspect
logging.basicConfig(format='%(levelname)s: %(message)s')

class AttributeException(Exception):
  """Exception for non existing attributes and functions."""
  def __init__(self, attr, matches=None):
    super(AttributeException, self).__init__()
    self.attr = attr
    self.matches = matches
    self.value = "'"+str(attr)+"' Attribute does not exist! Do you have a typo?"
    if matches:
      if len(matches) > 0:
        self.value += " These attributes were found: "+str(matches) 
  def __str__(self):
    return repr(self.value)

The previous class, AttributeException, is a simple exception that allows for matches from the module difflib or not. This is another property of python that I have come to enjoy, custom errors and exceptions.

class badspeller(object):
  """badspeller: this is meant to be a base class for using mispelled variables and functions"""
  
  #storing warnings, so they wont be repeated
  warnings = None
  
  def __init__(self, perfection=False, rude=False):
    super(badspeller, self).__init__()
    
    #initialize warnings
    if not badspeller.warnings:
      badspeller.warnings = []
      
    #print warnings?
    self.rude = rude
    
    #demand perfect spelling
    self.perfection = perfection

    #store function names
    self.__functions = {}
    for n, v in inspect.getmembers(self, inspect.ismethod):
      if not '__' in n:
        self.__functions[n] = v

  #set the lowercase attribute
  def __setattr__(self, attr, value):
    self.__dict__[attr.lower()] = value

  def __getattr__(self, attr):
    #if the attr exists, then return the attr
    if attr in self.__dict__.keys() or attr.lower() in self.__dict__.keys():
      return self.__dict__[attr.lower()]
    
    #if the attr is a function, return the function
    elif attr in self.__functions:
      return self.__functions[attr]
      
    #if it doesnt match, find the closest one
    else:
      matches = self.__dict__.keys()
      matches.extend(self.__functions.keys())
      close = difflib.get_close_matches(attr.lower(), matches, cutoff=0.5)
      try:
        raise AttributeException(attr, close)
      except:
        #demand perfect spelling
        if not self.perfection:
          
          #any close matches?
          if len(close) > 0:
            tbs = traceback.extract_stack()
            s = ''.join(str(s) for s in [tbs[0][0], ': line ', tbs[0][1]])
            s = "Substituting '"+str(close[0])+"' for '"+str(attr)+"' @ "+str(s)
            
            #only display unique warnings
            if s not in badspeller.warnings:
              badspeller.warnings.append(s)
              if self.rude:
                print '-'*(len(s)+9);
                logging.warning(s);
                print '-'*(len(s)+9)
            
            #function?
            if close[0] in self.__functions:
              return self.__functions[close[0]]
            else:
              return self.__dict__[close[0]]
          else:
            raise
        else:
          raise

The main functions that this class demonstrates are the __getattr__ and __setattr__ methods. First, the __setattr__ function simply insures all variables are lowercase. The __getattr__ method is a bit more complex.

To start with I simply check if the attribute was spelled correctly, by forcing the attribute to lowercase, the programmer can type in mixed case and it won’t matter. Next, I check the function names if nothing has matches yet. Finally, if nothing has matched so far, then the difflib module come in handy to find any close matches. If nothing is found to match, then I raise an error. If something was found, then I call the closest match and use logging to send a warning to the screen.

When I first started playing with this class, it was flooding the screen with warnings. I plugged the output by only outputting the unique warnings. This made any output much more readable.

if __name__ == '__main__':

  class rectangle(badspeller):
    """docstring for test"""
    def __init__(self, width, height, perfection=False, rude=True):
      super(rectangle, self).__init__(perfection, rude)
      self.width = width
      self.height = height
  
    def area(self):
      return self.width * self.height
  
  r = rectangle(5, 5)
  
  print r.widht
  print r.ht
  print r.arae()
  print "Done."

The code above simply inherits from the badspeller class. I have intentionally misspelled width, height and area and it runs with the following output:

-------------------------------------------------------------------
WARNING: Substituting 'width' for 'widht' @ badspeller.py: line 116
-------------------------------------------------------------------
5
-----------------------------------------------------------------
WARNING: Substituting 'height' for 'ht' @ badspeller.py: line 117
-----------------------------------------------------------------
5
-----------------------------------------------------------------
WARNING: Substituting 'area' for 'arae' @ badspeller.py: line 118
-----------------------------------------------------------------
25
Done.

I can also set the ‘rude’ keyword arg to silence the warnings and the perfection flag will force exceptions to be raised instead of warnings.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>