Skip to content

Instantly share code, notes, and snippets.

@j2labs
Created March 21, 2012 12:24
Show Gist options
  • Save j2labs/2146589 to your computer and use it in GitHub Desktop.
Save j2labs/2146589 to your computer and use it in GitHub Desktop.
Basic Python Metaclasses with Configurable Options
#!/usr/bin/env python
import inspect
import copy
###
### Options Classes
###
class DocOptions(object):
"""This class is a container for all metaclass configuration options. The
`__init__` method will set the default values for attributes and then
attempt to map any keyword arguments to attributes of the same name. If an
attribute is passed as a keyword but the attribute is not given a default
value prior to called `_parse_kwargs_dict`, it will be ignored.
"""
def __init__(self, **kwargs):
self.store_structures = False
self.mixin = False
self.bucket = None
### Relies on attr names above
self._parse_kwargs_dict(kwargs)
def _parse_kwargs_dict(self, kwargs_dict):
"""This function receives the dictionary with keyword arguments in
`__init__` and maps them to existing attributes on the class.
"""
for k, v in kwargs_dict.items():
if hasattr(self, k):
setattr(self, k, v)
class TopLevelDocOptions(DocOptions):
"""Extends `DocOptions` to add configuration values for
TopLevelDoc instances.
"""
def __init__(self, **kwargs):
self.id_field = None
self.auto_increment = None
### The call to super should be last
super(TopLevelDocOptions, self).__init__(**kwargs)
def _parse_meta_config(attrs, options_class):
"""Parses the Meta object on the class and translates it into an Option
instance.
"""
valid_attrs = dict()
if 'Meta' in attrs:
meta = attrs['Meta']
for attr_name, attr_value in inspect.getmembers(meta):
if not attr_name.startswith('_'):
valid_attrs[attr_name] = attr_value
oc = options_class(**valid_attrs)
return oc
def _gen_options(klass, attrs):
"""Processes the attributes and class parameters to generate the correct
options structure.
Defaults to `DocOptions` but it's ideal to define `__optionsclass_` on the
Document's metaclass.
"""
### Parse Meta
options_class = DocOptions
if hasattr(klass, '__optionsclass__'):
options_class = klass.__optionsclass__
options = _parse_meta_config(attrs, options_class)
return options
###
### Meta Classes
###
class DocMeta(type):
def __new__(cls, name, bases, attrs):
"""Processes a configuration of a Document type into a class.
"""
### Gen a class instance
klass = type.__new__(cls, name, bases, attrs)
### Parse metaclass config into options structure
options = _gen_options(klass, attrs)
klass._options = options
if hasattr(klass, 'Meta'):
delattr(klass, 'Meta')
### Generate class definition
return klass
class TopLevelDocMeta(DocMeta):
pass
###
### Base Documents
###
class BaseDoc(object):
__metaclass__ = DocMeta
__optionsclass__ = DocOptions
def __init__(self):
self._options = copy.copy(self._options)
class BaseTopLevelDoc(BaseDoc):
__metaclass__ = TopLevelDocMeta
__optionsclass__ = TopLevelDocOptions
class Meta:
mixin = True
id_field = 'some id field'
###
### Using the code
###
if __name__ == "__main__":
### Simple class with a single option that doesn't exist
print 'WORKING ON FOO ::'
class Foo(BaseDoc):
class Meta:
x = 'Foo'
f = Foo()
### Notice 'x' isn't listed
print 'F Options:', f._options.__dict__, '\n'
### Class with two options set
print 'WORKING ON BAR ::'
class Bar(BaseTopLevelDoc):
class Meta:
mixin = True
bucket = 'user_bars'
id_field = 'Word'
b = Bar()
print 'B Options:', b._options.__dict__, '\n'
### Configuration the options class at Class level
print 'WORKING ON BAZ ::'
class Baz(BaseTopLevelDoc):
__optionsclass__ = DocOptions # override default options cls
class Meta:
x = 'Baz' # unknown field
mixin = True
id_field = 'Word up' # unknown field
z = Baz()
zz = Baz()
print 'Z Options:', z._options.__dict__
print 'Flipping a config setting on an instance\n'
zz._options.mixin = False
print 'ZZ Options:', zz._options.__dict__
print 'Z Options:', z._options.__dict__
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment