Is this a good use of __metaclass__?

Joel Hedlund joel.hedlund at gmail.com
Fri May 5 06:33:25 EDT 2006


Hi!

I need some input on my use of metaclasses since I'm not sure I'm using them in 
a pythonic and graceful manner. I'm very grateful for any tips, pointers and 
RTFMs I can get from you guys.

Below, you'll find some background info and an executable code example.

In the code example I have two ways of doing the same thing. The problem is 
that the "Neat" version doesn't work, and the "Ugly" version that works gives 
me the creeps.

The "Neat" version raises a TypeError when I try the multiple inheritance 
(marked with comment in the code):

Traceback (most recent call last):
   File "/bioinfo/yohell/pybox/gridjs/gridjs-2.0/test.py", line 132, in ?
     class FullAPI(JobAPI, UserAPI, AdminAPI):
   File "/bioinfo/yohell/pybox/gridjs/gridjs-2.0/test.py", line 43, in __new__
     return type.__new__(cls,classname,bases,classdict)
TypeError: Error when calling the metaclass bases
     metaclass conflict: the metaclass of a derived class must be a 
(non-strict) subclass of the metaclasses of all its bases

In the "Ugly" version, I'm changing the metaclass in the global scope between 
class definitions, and that gives me bad vibes.

What should I do? Is there a way to fix my "Neat" solution? Is my "Ugly" 
solution in fact not so horrid as I think it is? Or should I rethink the whole 
idea? Or maybe stick with decorating manually (or in BaseAPI.__init__)?

Sincere thanks for your time.

Cheers!
/Joel Hedlund




Background
##########
(feel free to skip this if you are in a hurry)
I'm writing an XMLRPC server that serves three types of clients (jobs, users 
and admins). To do this I'm subclassing SimpleXMLRPCServer for all the 
connection work, and I was planning on putting the entire XMLRPC API as public 
methods of a class, and expose it to clients using .register_instance(). Each 
session is initiated by a handshake where a challenge is presented to the 
client, each method call must then be authenticated using certificates and 
incremental digest response. Each client type must be authenticated 
differently, and each type of client will also use a discrete set of methods.

At first, I was planning to use method decorators to do the validation, and 
have a different decorator for each type of client validation, like so:

class FullAPI:
     @valid_job_required
     def do_routine_stuff(self, clientid, response, ...):
         pass
     @valid_user_required
     def do_mundane_stuff(self, clientid, response, ...):
         pass
     @valid_admin_required
     def do_scary_stuff(self, clientid, response, ...):
         pass
     ...

There will be a lot of methods for each client type, so this class would become 
monstrous. Therefore I started wondering if it weren't a better idea to put the 
different client APIs in different classes and decorate them separately using 
metaclasses, and finally bring the APIs together using multiple inheritance. 
This is what I had in mind:

class BaseAPI(object):
     pass

class JobApi(BaseAPI):
     pass

class UserApi(BaseAPI):
     pass

class AdminApi(BaseAPI):
     pass

class FullApi(JobAPI, UserAPI, AdminAPI):
     pass

Now, I'm having trouble implementing the metaclass bit in a nice and pythonic way.



Code example
############

test.py
=======================================================================
# Base metaclass for decorating public methods:
from decorator import decorator

@decorator
def no_change(func, *pargs, **kwargs):
     return func(*pargs, **kwargs)

class DecoratePublicMethods(type):
     """Equip all public methods with a given decorator.

     Class data members:
     decorator = no_change: <decorator>
         The decorator that you wish to apply to public methods of the class
         instances. The default does not change program behavior.
     do_not_decorate = []: <iterable <str>>
         Names of public methods that should not be decorated.
     multiple_decoration = False: <bool>
         If set to False, methods will not be decorated if they already
         have been decorated by a prior metaclass.
     decoration_tag = '__public_method_decorated__': <str>
         Decorated public methods will be equipped with an attribute
         with this name and a value of True.

     """

     decorator = no_change
     do_not_decorate = []
     multiple_decoration = True
     decoration_tag = '__public_method_decorated__'

     def __new__(cls,classname,bases,classdict):
         for attr,item in classdict.items():
             if not callable(item):
                 continue
             if attr in cls.do_not_decorate or attr.startswith('_'):
                 continue
             if (not cls.multiple_decoration
                 and hasattr(classdict[attr], cls.decoration_tag)):
                 continue
             classdict[attr] = cls.decorator(item)
             setattr(classdict[attr], cls.decoration_tag, True)
         return type.__new__(cls,classname,bases,classdict)


## Authentication stuff:
class AuthenticationError(Exception):
     pass

import random

@decorator
def validate_job(func, self, id, response, *pargs, **kwargs):
     """Bogus authentiction routine"""
     if random.randint(0,3) / 3:
         raise AuthenticationError
     return func(self, id, response, *pargs, **kwargs)

@decorator
def validate_user(func, self, id, response, *pargs, **kwargs):
     """Bogus authentiction routine"""
     if random.randint(0,4) / 4:
         raise AuthenticationError
     return func(self, id, response, *pargs, **kwargs)

@decorator
def validate_admin(func, self, id, response, *pargs, **kwargs):
     """Bogus authentiction routine"""
     if random.randint(0,1):
         raise AuthenticationError
     return func(self, id, response, *pargs, **kwargs)


## Ugly (?) way that works:
## -----------------------------------------------------------
#class BaseAPI(object):
#    __metaclass__ = DecoratePublicMethods
#
#DecoratePublicMethods.decorator = validate_job
#
#class JobAPI(BaseAPI):
#    def do_routine_stuff(self, clientid, response, foo):
#        print "Routine stuff done."
#
#DecoratePublicMethods.decorator = validate_user
#
#class UserAPI(BaseAPI):
#    def do_mundane_stuff(self, clientid, response, moo):
#        print "Mundane stuff done."
#
#DecoratePublicMethods.decorator = validate_admin
#
#class AdminAPI(BaseAPI):
#    def do_scary_stuff(self, clientid, response, moose):
#        print "Scary stuff done."
#
#class FullAPI(JobAPI, UserAPI, AdminAPI):
#    pass
#
#a  = FullAPI()
## -----------------------------------------------------------

## Neat (?) way that doesn't work:
## -----------------------------------------------------------
class RequireJobValidation(DecoratePublicMethods):
     decorator = validate_job

class RequireUserValidation(DecoratePublicMethods):
     decorator = validate_user

class RequireAdminValidation(DecoratePublicMethods):
     decorator = validate_admin

class BaseAPI(object):
     pass

class JobAPI(BaseAPI):
     __metaclass__ = RequireJobValidation
     def do_routine_stuff(self, clientid, response, foo):
         print "Routine stuff done."

class UserAPI(BaseAPI):
     __metaclass__ = RequireUserValidation
     def do_mundane_stuff(self, clientid, response, moo):
         print "Mundane stuff done."

class AdminAPI(BaseAPI):
     __metaclass__ = RequireAdminValidation
     def do_scary_stuff(self, clientid, response, moose):
         print "Scary stuff done."

print "OK up to here."

## FIXME: Illegal multiple inheritance.
class FullAPI(JobAPI, UserAPI, AdminAPI):
     pass

b = FullAPI()
## -----------------------------------------------------------
=======================================================================

Oh, and by the way - this example uses Michele Simionato's excellent decorator 
module, available from here:
http://www.phyast.pitt.edu/~micheles/python/decorator.zip

If you don't want to donwload it, for this example you can just substitute this:

@decorator
def foo(func, *pargs, **kwargs):
     print "Code goes here"
     return func(*pargs, **kwargs)

for this:

def foo(func):
     def caller(*pargs, **kwargs):
         print "Code goes here"
         return func(*pargs, **kwargs)
     return caller

The difference is that @decorator preserves function signatures and such for 
decorated functions. Very neat.

Thanks again for your time.

/Joel



More information about the Python-list mailing list