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